Converting to Webpack. Stage 1.
Этот коммит содержится в:
137
webapp/components/about_build_modal.jsx
Обычный файл
137
webapp/components/about_build_modal.jsx
Обычный файл
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class AboutBuildModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.doHide = this.doHide.bind(this);
|
||||
}
|
||||
|
||||
doHide() {
|
||||
this.props.onModalDismissed();
|
||||
}
|
||||
|
||||
render() {
|
||||
const config = global.window.mm_config;
|
||||
const license = global.window.mm_license;
|
||||
|
||||
let title = (
|
||||
<FormattedMessage
|
||||
id='about.teamEditiont0'
|
||||
defaultMessage='Team Edition T0'
|
||||
/>
|
||||
);
|
||||
|
||||
let licensee;
|
||||
if (config.BuildEnterpriseReady === 'true') {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='about.teamEditiont1'
|
||||
defaultMessage='Team Edition T1'
|
||||
/>
|
||||
);
|
||||
if (license.IsLicensed === 'true') {
|
||||
title = (
|
||||
<FormattedMessage
|
||||
id='about.enterpriseEditione1'
|
||||
defaultMessage='Enterprise Edition E1'
|
||||
/>
|
||||
);
|
||||
licensee = (
|
||||
<div className='row form-group'>
|
||||
<div className='col-sm-3 info__label'>
|
||||
<FormattedMessage
|
||||
id='about.licensed'
|
||||
defaultMessage='Licensed by:'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-9'>{license.Company}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.doHide}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='about.title'
|
||||
defaultMessage='About Mattermost'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<h4>{'Mattermost'} {title}</h4>
|
||||
{licensee}
|
||||
<div className='row form-group'>
|
||||
<div className='col-sm-3 info__label'>
|
||||
<FormattedMessage
|
||||
id='about.version'
|
||||
defaultMessage='Version:'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-9'>{config.Version}</div>
|
||||
</div>
|
||||
<div className='row form-group'>
|
||||
<div className='col-sm-3 info__label'>
|
||||
<FormattedMessage
|
||||
id='about.number'
|
||||
defaultMessage='Build Number:'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-9'>{config.BuildNumber}</div>
|
||||
</div>
|
||||
<div className='row form-group'>
|
||||
<div className='col-sm-3 info__label'>
|
||||
<FormattedMessage
|
||||
id='about.date'
|
||||
defaultMessage='Build Date:'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-9'>{config.BuildDate}</div>
|
||||
</div>
|
||||
<div className='row form-group'>
|
||||
<div className='col-sm-3 info__label'>
|
||||
<FormattedMessage
|
||||
id='about.hash'
|
||||
defaultMessage='Build Hash:'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-9'>{config.BuildHash}</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.doHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='about.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AboutBuildModal.defaultProps = {
|
||||
show: false
|
||||
};
|
||||
|
||||
AboutBuildModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onModalDismissed: React.PropTypes.func.isRequired
|
||||
};
|
||||
113
webapp/components/access_history_modal.jsx
Обычный файл
113
webapp/components/access_history_modal.jsx
Обычный файл
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
import AuditTable from './audit_table.jsx';
|
||||
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {intlShape, injectIntl, FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class AccessHistoryModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onAuditChange = this.onAuditChange.bind(this);
|
||||
this.onShow = this.onShow.bind(this);
|
||||
this.onHide = this.onHide.bind(this);
|
||||
|
||||
const state = this.getStateFromStoresForAudits();
|
||||
state.moreInfo = [];
|
||||
|
||||
this.state = state;
|
||||
}
|
||||
getStateFromStoresForAudits() {
|
||||
return {
|
||||
audits: UserStore.getAudits()
|
||||
};
|
||||
}
|
||||
onShow() {
|
||||
AsyncClient.getAudits();
|
||||
|
||||
if ($(window).width() > 768) {
|
||||
$(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
|
||||
} else {
|
||||
$(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
|
||||
}
|
||||
}
|
||||
onHide() {
|
||||
this.setState({moreInfo: []});
|
||||
this.props.onHide();
|
||||
}
|
||||
componentDidMount() {
|
||||
UserStore.addAuditsChangeListener(this.onAuditChange);
|
||||
|
||||
if (this.props.show) {
|
||||
this.onShow();
|
||||
}
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.show && !prevProps.show) {
|
||||
this.onShow();
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
UserStore.removeAuditsChangeListener(this.onAuditChange);
|
||||
}
|
||||
onAuditChange() {
|
||||
var newState = this.getStateFromStoresForAudits();
|
||||
if (!Utils.areObjectsEqual(newState.audits, this.state.audits)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
var content;
|
||||
if (this.state.audits.loading) {
|
||||
content = (<LoadingScreen/>);
|
||||
} else {
|
||||
content = (
|
||||
<AuditTable
|
||||
audits={this.state.audits}
|
||||
showIp={true}
|
||||
showSession={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.onHide}
|
||||
bsSize='large'
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='access_history.title'
|
||||
defaultMessage='Access History'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body ref='modalBody'>
|
||||
{content}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AccessHistoryModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(AccessHistoryModal);
|
||||
299
webapp/components/activity_log_modal.jsx
Обычный файл
299
webapp/components/activity_log_modal.jsx
Обычный файл
@@ -0,0 +1,299 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class ActivityLogModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.submitRevoke = this.submitRevoke.bind(this);
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
this.handleMoreInfo = this.handleMoreInfo.bind(this);
|
||||
this.onHide = this.onHide.bind(this);
|
||||
this.onShow = this.onShow.bind(this);
|
||||
|
||||
let state = this.getStateFromStores();
|
||||
state.moreInfo = [];
|
||||
|
||||
this.state = state;
|
||||
}
|
||||
getStateFromStores() {
|
||||
return {
|
||||
sessions: UserStore.getSessions(),
|
||||
serverError: null,
|
||||
clientError: null
|
||||
};
|
||||
}
|
||||
submitRevoke(altId, e) {
|
||||
e.preventDefault();
|
||||
var modalContent = $(e.target).closest('.modal-content');
|
||||
modalContent.addClass('animation--highlight');
|
||||
setTimeout(() => {
|
||||
modalContent.removeClass('animation--highlight');
|
||||
}, 1500);
|
||||
Client.revokeSession(altId,
|
||||
function handleRevokeSuccess() {
|
||||
AsyncClient.getSessions();
|
||||
},
|
||||
function handleRevokeError(err) {
|
||||
let state = this.getStateFromStores();
|
||||
state.serverError = err;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
}
|
||||
onShow() {
|
||||
AsyncClient.getSessions();
|
||||
|
||||
if ($(window).width() > 768) {
|
||||
$(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
|
||||
} else {
|
||||
$(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 150);
|
||||
}
|
||||
}
|
||||
onHide() {
|
||||
this.setState({moreInfo: []});
|
||||
this.props.onHide();
|
||||
}
|
||||
componentDidMount() {
|
||||
UserStore.addSessionsChangeListener(this.onListenerChange);
|
||||
|
||||
if (this.props.show) {
|
||||
this.onShow();
|
||||
}
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.show && !prevProps.show) {
|
||||
this.onShow();
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
UserStore.removeSessionsChangeListener(this.onListenerChange);
|
||||
}
|
||||
onListenerChange() {
|
||||
const newState = this.getStateFromStores();
|
||||
if (!Utils.areObjectsEqual(newState.sessions, this.state.sessions)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
handleMoreInfo(index) {
|
||||
let newMoreInfo = this.state.moreInfo;
|
||||
newMoreInfo[index] = true;
|
||||
this.setState({moreInfo: newMoreInfo});
|
||||
}
|
||||
render() {
|
||||
let activityList = [];
|
||||
|
||||
for (let i = 0; i < this.state.sessions.length; i++) {
|
||||
const currentSession = this.state.sessions[i];
|
||||
const lastAccessTime = new Date(currentSession.last_activity_at);
|
||||
const firstAccessTime = new Date(currentSession.create_at);
|
||||
let devicePlatform = currentSession.props.platform;
|
||||
let devicePicture = '';
|
||||
|
||||
if (currentSession.props.platform === 'Windows') {
|
||||
devicePicture = 'fa fa-windows';
|
||||
} else if (currentSession.device_id && currentSession.device_id.indexOf('apple:') === 0) {
|
||||
devicePicture = 'fa fa-apple';
|
||||
devicePlatform = (
|
||||
<FormattedMessage
|
||||
id='activity_log_modal.iphoneNativeApp'
|
||||
defaultMessage='iPhone Native App'
|
||||
/>
|
||||
);
|
||||
} else if (currentSession.device_id && currentSession.device_id.indexOf('android:') === 0) {
|
||||
devicePlatform = (
|
||||
<FormattedMessage
|
||||
id='activity_log_modal.androidNativeApp'
|
||||
defaultMessage='Android Native App'
|
||||
/>
|
||||
);
|
||||
devicePicture = 'fa fa-android';
|
||||
} else if (currentSession.props.platform === 'Macintosh' ||
|
||||
currentSession.props.platform === 'iPhone') {
|
||||
devicePicture = 'fa fa-apple';
|
||||
} else if (currentSession.props.platform === 'Linux') {
|
||||
if (currentSession.props.os.indexOf('Android') >= 0) {
|
||||
devicePlatform = (
|
||||
<FormattedMessage
|
||||
id='activity_log_modal.android'
|
||||
defaultMessage='Android'
|
||||
/>
|
||||
);
|
||||
devicePicture = 'fa fa-android';
|
||||
} else {
|
||||
devicePicture = 'fa fa-linux';
|
||||
}
|
||||
}
|
||||
|
||||
let moreInfo;
|
||||
if (this.state.moreInfo[i]) {
|
||||
moreInfo = (
|
||||
<div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='activity_log.firstTime'
|
||||
defaultMessage='First time active: {date}, {time}'
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
value={firstAccessTime}
|
||||
day='2-digit'
|
||||
month='long'
|
||||
year='numeric'
|
||||
/>
|
||||
),
|
||||
time: (
|
||||
<FormattedTime
|
||||
value={firstAccessTime}
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='activity_log.os'
|
||||
defaultMessage='OS: {os}'
|
||||
values={{
|
||||
os: currentSession.props.os
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='activity_log.browser'
|
||||
defaultMessage='Browser: {browser}'
|
||||
values={{
|
||||
browser: currentSession.props.browser
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='activity_log.sessionId'
|
||||
defaultMessage='Session ID: {id}'
|
||||
values={{
|
||||
id: currentSession.id
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
moreInfo = (
|
||||
<a
|
||||
className='theme'
|
||||
href='#'
|
||||
onClick={this.handleMoreInfo.bind(this, i)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='activity_log.moreInfo'
|
||||
defaultMessage='More info'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
activityList[i] = (
|
||||
<div
|
||||
key={'activityLogEntryKey' + i}
|
||||
className='activity-log__table'
|
||||
>
|
||||
<div className='activity-log__report'>
|
||||
<div className='report__platform'><i className={devicePicture}/>{devicePlatform}</div>
|
||||
<div className='report__info'>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='activity_log.lastActivity'
|
||||
defaultMessage='Last activity: {date}, {time}'
|
||||
values={{
|
||||
date: (
|
||||
<FormattedDate
|
||||
value={lastAccessTime}
|
||||
day='2-digit'
|
||||
month='long'
|
||||
year='numeric'
|
||||
/>
|
||||
),
|
||||
time: (
|
||||
<FormattedTime
|
||||
value={lastAccessTime}
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{moreInfo}
|
||||
</div>
|
||||
</div>
|
||||
<div className='activity-log__action'>
|
||||
<button
|
||||
onClick={this.submitRevoke.bind(this, currentSession.id)}
|
||||
className='btn btn-primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='activity_log.logout'
|
||||
defaultMessage='Logout'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let content;
|
||||
if (this.state.sessions.loading) {
|
||||
content = <LoadingScreen/>;
|
||||
} else {
|
||||
content = <form role='form'>{activityList}</form>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.onHide}
|
||||
bsSize='large'
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='activity_log.activeSessions'
|
||||
defaultMessage='Active Sessions'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body ref='modalBody'>
|
||||
<p className='session-help-text'>
|
||||
<FormattedMessage
|
||||
id='activity_log.sessionsDescription'
|
||||
defaultMessage="Sessions are created when you log in to a new browser on a device. Sessions let you use Mattermost without having to log in again for a time period specified by the System Admin. If you want to log out sooner, use the 'Logout' button below to end a session."
|
||||
/>
|
||||
</p>
|
||||
{content}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ActivityLogModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired
|
||||
};
|
||||
218
webapp/components/admin_console/admin_controller.jsx
Обычный файл
218
webapp/components/admin_console/admin_controller.jsx
Обычный файл
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import AdminSidebar from './admin_sidebar.jsx';
|
||||
import AdminStore from 'stores/admin_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import LoadingScreen from '../loading_screen.jsx';
|
||||
|
||||
import EmailSettingsTab from './email_settings.jsx';
|
||||
import LogSettingsTab from './log_settings.jsx';
|
||||
import LogsTab from './logs.jsx';
|
||||
import AuditsTab from './audits.jsx';
|
||||
import FileSettingsTab from './image_settings.jsx';
|
||||
import PrivacySettingsTab from './privacy_settings.jsx';
|
||||
import RateSettingsTab from './rate_settings.jsx';
|
||||
import GitLabSettingsTab from './gitlab_settings.jsx';
|
||||
import SqlSettingsTab from './sql_settings.jsx';
|
||||
import TeamSettingsTab from './team_settings.jsx';
|
||||
import ServiceSettingsTab from './service_settings.jsx';
|
||||
import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
|
||||
import TeamUsersTab from './team_users.jsx';
|
||||
import TeamAnalyticsTab from '../analytics/team_analytics.jsx';
|
||||
import LdapSettingsTab from './ldap_settings.jsx';
|
||||
import LicenseSettingsTab from './license_settings.jsx';
|
||||
import SystemAnalyticsTab from '../analytics/system_analytics.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class AdminController extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.selectTab = this.selectTab.bind(this);
|
||||
this.removeSelectedTeam = this.removeSelectedTeam.bind(this);
|
||||
this.addSelectedTeam = this.addSelectedTeam.bind(this);
|
||||
this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
|
||||
this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this);
|
||||
|
||||
var selectedTeams = AdminStore.getSelectedTeams();
|
||||
if (selectedTeams == null) {
|
||||
selectedTeams = {};
|
||||
selectedTeams[TeamStore.getCurrentId()] = 'true';
|
||||
AdminStore.saveSelectedTeams(selectedTeams);
|
||||
}
|
||||
|
||||
this.state = {
|
||||
config: AdminStore.getConfig(),
|
||||
teams: AdminStore.getAllTeams(),
|
||||
selectedTeams,
|
||||
selected: props.tab || 'system_analytics',
|
||||
selectedTeam: props.teamId || null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AdminStore.addConfigChangeListener(this.onConfigListenerChange);
|
||||
AsyncClient.getConfig();
|
||||
|
||||
AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
|
||||
AsyncClient.getAllTeams();
|
||||
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
$('[data-toggle="popover"]').popover();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
|
||||
AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange);
|
||||
}
|
||||
|
||||
onConfigListenerChange() {
|
||||
this.setState({
|
||||
config: AdminStore.getConfig(),
|
||||
teams: AdminStore.getAllTeams(),
|
||||
selectedTeams: AdminStore.getSelectedTeams(),
|
||||
selected: this.state.selected,
|
||||
selectedTeam: this.state.selectedTeam
|
||||
});
|
||||
}
|
||||
|
||||
onAllTeamsListenerChange() {
|
||||
this.setState({
|
||||
config: AdminStore.getConfig(),
|
||||
teams: AdminStore.getAllTeams(),
|
||||
selectedTeams: AdminStore.getSelectedTeams(),
|
||||
selected: this.state.selected,
|
||||
selectedTeam: this.state.selectedTeam
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
selectTab(tab, teamId) {
|
||||
this.setState({
|
||||
config: AdminStore.getConfig(),
|
||||
teams: AdminStore.getAllTeams(),
|
||||
selectedTeams: AdminStore.getSelectedTeams(),
|
||||
selected: tab,
|
||||
selectedTeam: teamId
|
||||
});
|
||||
}
|
||||
|
||||
removeSelectedTeam(teamId) {
|
||||
var selectedTeams = AdminStore.getSelectedTeams();
|
||||
Reflect.deleteProperty(selectedTeams, teamId);
|
||||
AdminStore.saveSelectedTeams(selectedTeams);
|
||||
|
||||
this.setState({
|
||||
config: AdminStore.getConfig(),
|
||||
teams: AdminStore.getAllTeams(),
|
||||
selectedTeams: AdminStore.getSelectedTeams(),
|
||||
selected: this.state.selected,
|
||||
selectedTeam: this.state.selectedTeam
|
||||
});
|
||||
}
|
||||
|
||||
addSelectedTeam(teamId) {
|
||||
var selectedTeams = AdminStore.getSelectedTeams();
|
||||
selectedTeams[teamId] = 'true';
|
||||
AdminStore.saveSelectedTeams(selectedTeams);
|
||||
|
||||
this.setState({
|
||||
config: AdminStore.getConfig(),
|
||||
teams: AdminStore.getAllTeams(),
|
||||
selectedTeams: AdminStore.getSelectedTeams(),
|
||||
selected: this.state.selected,
|
||||
selectedTeam: this.state.selectedTeam
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
var tab = <LoadingScreen/>;
|
||||
|
||||
if (this.state.config != null) {
|
||||
if (this.state.selected === 'email_settings') {
|
||||
tab = <EmailSettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'log_settings') {
|
||||
tab = <LogSettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'logs') {
|
||||
tab = <LogsTab/>;
|
||||
} else if (this.state.selected === 'audits') {
|
||||
tab = <AuditsTab/>;
|
||||
} else if (this.state.selected === 'image_settings') {
|
||||
tab = <FileSettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'privacy_settings') {
|
||||
tab = <PrivacySettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'rate_settings') {
|
||||
tab = <RateSettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'gitlab_settings') {
|
||||
tab = <GitLabSettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'sql_settings') {
|
||||
tab = <SqlSettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'team_settings') {
|
||||
tab = <TeamSettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'service_settings') {
|
||||
tab = <ServiceSettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'legal_and_support_settings') {
|
||||
tab = <LegalAndSupportSettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'ldap_settings') {
|
||||
tab = <LdapSettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'license') {
|
||||
tab = <LicenseSettingsTab config={this.state.config}/>;
|
||||
} else if (this.state.selected === 'team_users') {
|
||||
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]}/>;
|
||||
}
|
||||
} else if (this.state.selected === 'system_analytics') {
|
||||
tab = <SystemAnalyticsTab/>;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id='admin_controller'
|
||||
className='admin-controller'
|
||||
>
|
||||
<div
|
||||
className='sidebar--menu'
|
||||
id='sidebar-menu'
|
||||
/>
|
||||
<AdminSidebar
|
||||
selected={this.state.selected}
|
||||
selectedTeam={this.state.selectedTeam}
|
||||
selectTab={this.selectTab}
|
||||
teams={this.state.teams}
|
||||
selectedTeams={this.state.selectedTeams}
|
||||
removeSelectedTeam={this.removeSelectedTeam}
|
||||
addSelectedTeam={this.addSelectedTeam}
|
||||
/>
|
||||
<div className='inner-wrap channel__wrap'>
|
||||
<div className='row header'>
|
||||
</div>
|
||||
<div className='row main'>
|
||||
<div
|
||||
id='app-content'
|
||||
className='app__content admin'
|
||||
>
|
||||
{tab}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AdminController.defaultProps = {
|
||||
};
|
||||
|
||||
AdminController.propTypes = {
|
||||
tab: React.PropTypes.string,
|
||||
teamId: React.PropTypes.string
|
||||
};
|
||||
114
webapp/components/admin_console/admin_navbar_dropdown.jsx
Обычный файл
114
webapp/components/admin_console/admin_navbar_dropdown.jsx
Обычный файл
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Link} from 'react-router';
|
||||
|
||||
function getStateFromStores() {
|
||||
return {currentTeam: TeamStore.getCurrent()};
|
||||
}
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class AdminNavbarDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.blockToggle = false;
|
||||
|
||||
this.state = getStateFromStores();
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
|
||||
this.blockToggle = true;
|
||||
setTimeout(() => {
|
||||
this.blockToggle = false;
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
$(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ul className='nav navbar-nav navbar-right'>
|
||||
<li
|
||||
ref='dropdown'
|
||||
className='dropdown'
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
className='dropdown-toggle'
|
||||
data-toggle='dropdown'
|
||||
role='button'
|
||||
aria-expanded='false'
|
||||
>
|
||||
<span
|
||||
className='dropdown__icon'
|
||||
dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}}
|
||||
/>
|
||||
</a>
|
||||
<ul
|
||||
className='dropdown-menu'
|
||||
role='menu'
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href={Utils.getWindowLocationOrigin() + '/' + this.state.currentTeam.name}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.nav.switch'
|
||||
defaultMessage='Switch to {display_name}'
|
||||
values={{
|
||||
display_name: this.state.currentTeam.display_name
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<Link to={Utils.getTeamURLFromAddressBar() + '/logout'}>
|
||||
<FormattedMessage
|
||||
id='admin.nav.logout'
|
||||
defaultMessage='Logout'
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
<li className='divider'></li>
|
||||
<li>
|
||||
<a
|
||||
target='_blank'
|
||||
href='/static/help/help.html'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.nav.help'
|
||||
defaultMessage='Help'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
target='_blank'
|
||||
href='/static/help/report_problem.html'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.nav.report'
|
||||
defaultMessage='Report a Problem'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
487
webapp/components/admin_console/admin_sidebar.jsx
Обычный файл
487
webapp/components/admin_console/admin_sidebar.jsx
Обычный файл
@@ -0,0 +1,487 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import AdminSidebarHeader from './admin_sidebar_header.jsx';
|
||||
import SelectTeamModal from './select_team_modal.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class AdminSidebar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.isSelected = this.isSelected.bind(this);
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.removeTeam = this.removeTeam.bind(this);
|
||||
|
||||
this.showTeamSelect = this.showTeamSelect.bind(this);
|
||||
this.teamSelectedModal = this.teamSelectedModal.bind(this);
|
||||
this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this);
|
||||
|
||||
this.state = {
|
||||
showSelectModal: false
|
||||
};
|
||||
}
|
||||
|
||||
handleClick(name, teamId, e) {
|
||||
e.preventDefault();
|
||||
this.props.selectTab(name, teamId);
|
||||
}
|
||||
|
||||
isSelected(name, teamId) {
|
||||
if (this.props.selected === name) {
|
||||
if (name === 'team_users' || name === 'team_analytics') {
|
||||
if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
|
||||
return 'active';
|
||||
}
|
||||
} else {
|
||||
return 'active';
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
removeTeam(teamId, e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
Reflect.deleteProperty(this.props.selectedTeams, teamId);
|
||||
this.props.removeSelectedTeam(teamId);
|
||||
|
||||
if (this.props.selected === 'team_users') {
|
||||
if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
|
||||
this.props.selectTab('service_settings', null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showTeamSelect(e) {
|
||||
e.preventDefault();
|
||||
this.setState({showSelectModal: true});
|
||||
}
|
||||
|
||||
teamSelectedModal(teamId) {
|
||||
this.setState({showSelectModal: false});
|
||||
this.props.addSelectedTeam(teamId);
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
teamSelectedModalDismissed() {
|
||||
this.setState({showSelectModal: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
var count = '*';
|
||||
var teams = (
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.loading'
|
||||
defaultMessage='Loading'
|
||||
/>
|
||||
);
|
||||
const removeTooltip = (
|
||||
<Tooltip id='remove-team-tooltip'>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.rmTeamSidebar'
|
||||
defaultMessage='Remove team from sidebar menu'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
const addTeamTooltip = (
|
||||
<Tooltip id='add-team-tooltip'>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.addTeamSidebar'
|
||||
defaultMessage='Add team from sidebar menu'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
if (this.props.teams != null) {
|
||||
count = '' + Object.keys(this.props.teams).length;
|
||||
|
||||
teams = [];
|
||||
for (var key in this.props.selectedTeams) {
|
||||
if (this.props.selectedTeams.hasOwnProperty(key)) {
|
||||
var team = this.props.teams[key];
|
||||
|
||||
if (team != null) {
|
||||
teams.push(
|
||||
<ul
|
||||
key={'team_' + team.id}
|
||||
className='nav nav__sub-menu'
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.handleClick.bind(this, 'team_users', team.id)}
|
||||
className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id) + ' ' + this.isSelected('team_analytics', team.id)}
|
||||
>
|
||||
{team.name}
|
||||
<OverlayTrigger
|
||||
delayShow={1000}
|
||||
placement='top'
|
||||
overlay={removeTooltip}
|
||||
>
|
||||
<span
|
||||
className='menu-icon--right menu__close'
|
||||
onClick={this.removeTeam.bind(this, team.id)}
|
||||
style={{cursor: 'pointer'}}
|
||||
>
|
||||
{'×'}
|
||||
</span>
|
||||
</OverlayTrigger>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<ul className='nav nav__inner-menu'>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('team_users', team.id)}
|
||||
onClick={this.handleClick.bind(this, 'team_users', team.id)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.users'
|
||||
defaultMessage='- Users'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('team_analytics', team.id)}
|
||||
onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.statistics'
|
||||
defaultMessage='- Statistics'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ldapSettings;
|
||||
let licenseSettings;
|
||||
if (global.window.mm_config.BuildEnterpriseReady === 'true') {
|
||||
if (global.window.mm_license.IsLicensed === 'true') {
|
||||
ldapSettings = (
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('ldap_settings')}
|
||||
onClick={this.handleClick.bind(this, 'ldap_settings', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.ldap'
|
||||
defaultMessage='LDAP Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
licenseSettings = (
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('license')}
|
||||
onClick={this.handleClick.bind(this, 'license', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.license'
|
||||
defaultMessage='Edition and License'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
let audits;
|
||||
if (global.window.mm_license.IsLicensed === 'true') {
|
||||
audits = (
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('audits')}
|
||||
onClick={this.handleClick.bind(this, 'audits', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.audits'
|
||||
defaultMessage='Compliance and Auditing'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='sidebar--left sidebar--collapsable'>
|
||||
<div>
|
||||
<AdminSidebarHeader/>
|
||||
<div className='nav-pills__container'>
|
||||
<ul className='nav nav-pills nav-stacked'>
|
||||
<li>
|
||||
<ul className='nav nav__sub-menu'>
|
||||
<li>
|
||||
<h4>
|
||||
<span className='icon fa fa-gear'></span>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.reports'
|
||||
defaultMessage='SITE REPORTS'
|
||||
/>
|
||||
</span>
|
||||
</h4>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className='nav nav__sub-menu padded'>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('system_analytics')}
|
||||
onClick={this.handleClick.bind(this, 'system_analytics', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.view_statistics'
|
||||
defaultMessage='View Statistics'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className='nav nav__sub-menu'>
|
||||
<li>
|
||||
<h4>
|
||||
<span className='icon fa fa-gear'></span>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.settings'
|
||||
defaultMessage='SETTINGS'
|
||||
/>
|
||||
</span>
|
||||
</h4>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className='nav nav__sub-menu padded'>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('service_settings')}
|
||||
onClick={this.handleClick.bind(this, 'service_settings', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.service'
|
||||
defaultMessage='Service Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('team_settings')}
|
||||
onClick={this.handleClick.bind(this, 'team_settings', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.team'
|
||||
defaultMessage='Team Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('sql_settings')}
|
||||
onClick={this.handleClick.bind(this, 'sql_settings', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.sql'
|
||||
defaultMessage='SQL Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('email_settings')}
|
||||
onClick={this.handleClick.bind(this, 'email_settings', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.email'
|
||||
defaultMessage='Email Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('image_settings')}
|
||||
onClick={this.handleClick.bind(this, 'image_settings', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.file'
|
||||
defaultMessage='File Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('log_settings')}
|
||||
onClick={this.handleClick.bind(this, 'log_settings', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.log'
|
||||
defaultMessage='Log Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('rate_settings')}
|
||||
onClick={this.handleClick.bind(this, 'rate_settings', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.rate_limit'
|
||||
defaultMessage='Rate Limit Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('privacy_settings')}
|
||||
onClick={this.handleClick.bind(this, 'privacy_settings', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.privacy'
|
||||
defaultMessage='Privacy Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('gitlab_settings')}
|
||||
onClick={this.handleClick.bind(this, 'gitlab_settings', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.gitlab'
|
||||
defaultMessage='GitLab Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
{ldapSettings}
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('legal_and_support_settings')}
|
||||
onClick={this.handleClick.bind(this, 'legal_and_support_settings', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.support'
|
||||
defaultMessage='Legal and Support Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className='nav nav__sub-menu'>
|
||||
<li>
|
||||
<h4>
|
||||
<span className='icon fa fa-gear'></span>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.teams'
|
||||
defaultMessage='TEAMS ({count})'
|
||||
values={{
|
||||
count: count
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className='menu-icon--right'>
|
||||
<OverlayTrigger
|
||||
delayShow={1000}
|
||||
placement='top'
|
||||
overlay={addTeamTooltip}
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.showTeamSelect}
|
||||
>
|
||||
<i
|
||||
className='fa fa-plus'
|
||||
></i>
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
</span>
|
||||
</h4>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className='nav nav__sub-menu padded'>
|
||||
<li>
|
||||
{teams}
|
||||
</li>
|
||||
</ul>
|
||||
<ul className='nav nav__sub-menu'>
|
||||
<li>
|
||||
<h4>
|
||||
<span className='icon fa fa-gear'></span>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.other'
|
||||
defaultMessage='OTHER'
|
||||
/>
|
||||
</span>
|
||||
</h4>
|
||||
</li>
|
||||
</ul>
|
||||
<ul className='nav nav__sub-menu padded'>
|
||||
{licenseSettings}
|
||||
{audits}
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('logs')}
|
||||
onClick={this.handleClick.bind(this, 'logs', null)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.logs'
|
||||
defaultMessage='Logs'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SelectTeamModal
|
||||
teams={this.props.teams}
|
||||
show={this.state.showSelectModal}
|
||||
onModalSubmit={this.teamSelectedModal}
|
||||
onModalDismissed={this.teamSelectedModalDismissed}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AdminSidebar.propTypes = {
|
||||
teams: React.PropTypes.object,
|
||||
selectedTeams: React.PropTypes.object,
|
||||
removeSelectedTeam: React.PropTypes.func,
|
||||
addSelectedTeam: React.PropTypes.func,
|
||||
selected: React.PropTypes.string,
|
||||
selectedTeam: React.PropTypes.string,
|
||||
selectTab: React.PropTypes.func
|
||||
};
|
||||
70
webapp/components/admin_console/admin_sidebar_header.jsx
Обычный файл
70
webapp/components/admin_console/admin_sidebar_header.jsx
Обычный файл
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import AdminNavbarDropdown from './admin_navbar_dropdown.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class SidebarHeader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.toggleDropdown = this.toggleDropdown.bind(this);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
toggleDropdown(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.refs.dropdown.blockToggle) {
|
||||
this.refs.dropdown.blockToggle = false;
|
||||
return;
|
||||
}
|
||||
|
||||
$('.team__header').find('.dropdown-toggle').dropdown('toggle');
|
||||
}
|
||||
|
||||
render() {
|
||||
var me = UserStore.getCurrentUser();
|
||||
var profilePicture = null;
|
||||
|
||||
if (!me) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (me.last_picture_update) {
|
||||
profilePicture = (
|
||||
<img
|
||||
className='user__picture'
|
||||
src={'/api/v1/users/' + me.id + '/image?time=' + me.update_at}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='team__header theme'>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.toggleDropdown}
|
||||
>
|
||||
{profilePicture}
|
||||
<div className='header__info'>
|
||||
<div className='user__name'>{'@' + me.username}</div>
|
||||
<div className='team__name'>
|
||||
<FormattedMessage
|
||||
id='admin.sidebarHeader.systemConsole'
|
||||
defaultMessage='System Console'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<AdminNavbarDropdown ref='dropdown'/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
97
webapp/components/admin_console/audits.jsx
Обычный файл
97
webapp/components/admin_console/audits.jsx
Обычный файл
@@ -0,0 +1,97 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import LoadingScreen from '../loading_screen.jsx';
|
||||
import AuditTable from '../audit_table.jsx';
|
||||
|
||||
import AdminStore from 'stores/admin_store.jsx';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class Audits extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onAuditListenerChange = this.onAuditListenerChange.bind(this);
|
||||
this.reload = this.reload.bind(this);
|
||||
|
||||
this.state = {
|
||||
audits: AdminStore.getAudits()
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AdminStore.addAuditChangeListener(this.onAuditListenerChange);
|
||||
AsyncClient.getServerAudits();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AdminStore.removeAuditChangeListener(this.onAuditListenerChange);
|
||||
}
|
||||
|
||||
onAuditListenerChange() {
|
||||
this.setState({
|
||||
audits: AdminStore.getAudits()
|
||||
});
|
||||
}
|
||||
|
||||
reload() {
|
||||
AdminStore.saveAudits(null);
|
||||
this.setState({
|
||||
audits: null
|
||||
});
|
||||
|
||||
AsyncClient.getServerAudits();
|
||||
}
|
||||
|
||||
render() {
|
||||
var content = null;
|
||||
|
||||
if (global.window.mm_license.IsLicensed !== 'true') {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
if (this.state.audits === null) {
|
||||
content = <LoadingScreen/>;
|
||||
} else {
|
||||
content = (
|
||||
<div style={{margin: '10px'}}>
|
||||
<AuditTable
|
||||
audits={this.state.audits}
|
||||
showUserId={true}
|
||||
showIp={true}
|
||||
showSession={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='panel'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.audits.title'
|
||||
defaultMessage='User Activity'
|
||||
/>
|
||||
</h3>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
onClick={this.reload}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.audits.reload'
|
||||
defaultMessage='Reload'
|
||||
/>
|
||||
</button>
|
||||
<div className='log__panel'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
961
webapp/components/admin_console/email_settings.jsx
Обычный файл
961
webapp/components/admin_console/email_settings.jsx
Обычный файл
@@ -0,0 +1,961 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import crypto from 'crypto';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||
|
||||
var holders = defineMessages({
|
||||
notificationDisplayExample: {
|
||||
id: 'admin.email.notificationDisplayExample',
|
||||
defaultMessage: 'Ex: "Mattermost Notification", "System", "No-Reply"'
|
||||
},
|
||||
notificationEmailExample: {
|
||||
id: 'admin.email.notificationEmailExample',
|
||||
defaultMessage: 'Ex: "mattermost@yourcompany.com", "admin@yourcompany.com"'
|
||||
},
|
||||
smtpUsernameExample: {
|
||||
id: 'admin.email.smtpUsernameExample',
|
||||
defaultMessage: 'Ex: "admin@yourcompany.com", "AKIADTOVBGERKLCBV"'
|
||||
},
|
||||
smtpPasswordExample: {
|
||||
id: 'admin.email.smtpPasswordExample',
|
||||
defaultMessage: 'Ex: "yourpassword", "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
|
||||
},
|
||||
smtpServerExample: {
|
||||
id: 'admin.email.smtpServerExample',
|
||||
defaultMessage: 'Ex: "smtp.yourcompany.com", "email-smtp.us-east-1.amazonaws.com"'
|
||||
},
|
||||
smtpPortExample: {
|
||||
id: 'admin.email.smtpPortExample',
|
||||
defaultMessage: 'Ex: "25", "465"'
|
||||
},
|
||||
connectionSecurityNone: {
|
||||
id: 'admin.email.connectionSecurityNone',
|
||||
defaultMessage: 'None'
|
||||
},
|
||||
connectionSecurityTls: {
|
||||
id: 'admin.email.connectionSecurityTls',
|
||||
defaultMessage: 'TLS (Recommended)'
|
||||
},
|
||||
connectionSecurityStart: {
|
||||
id: 'admin.email.connectionSecurityStart',
|
||||
defaultMessage: 'STARTTLS'
|
||||
},
|
||||
inviteSaltExample: {
|
||||
id: 'admin.email.inviteSaltExample',
|
||||
defaultMessage: 'Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
|
||||
},
|
||||
passwordSaltExample: {
|
||||
id: 'admin.email.passwordSaltExample',
|
||||
defaultMessage: 'Ex "bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo"'
|
||||
},
|
||||
pushServerEx: {
|
||||
id: 'admin.email.pushServerEx',
|
||||
defaultMessage: 'E.g.: "http://push-test.mattermost.com"'
|
||||
},
|
||||
testing: {
|
||||
id: 'admin.email.testing',
|
||||
defaultMessage: 'Testing...'
|
||||
},
|
||||
saving: {
|
||||
id: 'admin.email.saving',
|
||||
defaultMessage: 'Saving Config...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class EmailSettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleTestConnection = this.handleTestConnection.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.buildConfig = this.buildConfig.bind(this);
|
||||
this.handleGenerateInvite = this.handleGenerateInvite.bind(this);
|
||||
this.handleGenerateReset = this.handleGenerateReset.bind(this);
|
||||
|
||||
this.state = {
|
||||
sendEmailNotifications: this.props.config.EmailSettings.SendEmailNotifications,
|
||||
sendPushNotifications: this.props.config.EmailSettings.SendPushNotifications,
|
||||
saveNeeded: false,
|
||||
serverError: null,
|
||||
emailSuccess: null,
|
||||
emailFail: null
|
||||
};
|
||||
}
|
||||
|
||||
handleChange(action) {
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
|
||||
if (action === 'sendEmailNotifications_true') {
|
||||
s.sendEmailNotifications = true;
|
||||
}
|
||||
|
||||
if (action === 'sendEmailNotifications_false') {
|
||||
s.sendEmailNotifications = false;
|
||||
}
|
||||
|
||||
if (action === 'sendPushNotifications_true') {
|
||||
s.sendPushNotifications = true;
|
||||
}
|
||||
|
||||
if (action === 'sendPushNotifications_false') {
|
||||
s.sendPushNotifications = false;
|
||||
}
|
||||
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
buildConfig() {
|
||||
var config = this.props.config;
|
||||
config.EmailSettings.EnableSignUpWithEmail = ReactDOM.findDOMNode(this.refs.allowSignUpWithEmail).checked;
|
||||
config.EmailSettings.EnableSignInWithEmail = ReactDOM.findDOMNode(this.refs.allowSignInWithEmail).checked;
|
||||
config.EmailSettings.EnableSignInWithUsername = ReactDOM.findDOMNode(this.refs.allowSignInWithUsername).checked;
|
||||
config.EmailSettings.SendEmailNotifications = ReactDOM.findDOMNode(this.refs.sendEmailNotifications).checked;
|
||||
config.EmailSettings.SendPushNotifications = ReactDOM.findDOMNode(this.refs.sendPushNotifications).checked;
|
||||
config.EmailSettings.RequireEmailVerification = ReactDOM.findDOMNode(this.refs.requireEmailVerification).checked;
|
||||
config.EmailSettings.FeedbackName = ReactDOM.findDOMNode(this.refs.feedbackName).value.trim();
|
||||
config.EmailSettings.FeedbackEmail = ReactDOM.findDOMNode(this.refs.feedbackEmail).value.trim();
|
||||
config.EmailSettings.SMTPServer = ReactDOM.findDOMNode(this.refs.SMTPServer).value.trim();
|
||||
config.EmailSettings.PushNotificationServer = ReactDOM.findDOMNode(this.refs.PushNotificationServer).value.trim();
|
||||
config.EmailSettings.SMTPPort = ReactDOM.findDOMNode(this.refs.SMTPPort).value.trim();
|
||||
config.EmailSettings.SMTPUsername = ReactDOM.findDOMNode(this.refs.SMTPUsername).value.trim();
|
||||
config.EmailSettings.SMTPPassword = ReactDOM.findDOMNode(this.refs.SMTPPassword).value.trim();
|
||||
config.EmailSettings.ConnectionSecurity = ReactDOM.findDOMNode(this.refs.ConnectionSecurity).value.trim();
|
||||
|
||||
config.EmailSettings.InviteSalt = ReactDOM.findDOMNode(this.refs.InviteSalt).value.trim();
|
||||
if (config.EmailSettings.InviteSalt === '') {
|
||||
config.EmailSettings.InviteSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
|
||||
ReactDOM.findDOMNode(this.refs.InviteSalt).value = config.EmailSettings.InviteSalt;
|
||||
}
|
||||
|
||||
config.EmailSettings.PasswordResetSalt = ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value.trim();
|
||||
if (config.EmailSettings.PasswordResetSalt === '') {
|
||||
config.EmailSettings.PasswordResetSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
|
||||
ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = config.EmailSettings.PasswordResetSalt;
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
handleGenerateInvite(e) {
|
||||
e.preventDefault();
|
||||
ReactDOM.findDOMNode(this.refs.InviteSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleGenerateReset(e) {
|
||||
e.preventDefault();
|
||||
ReactDOM.findDOMNode(this.refs.PasswordResetSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleTestConnection(e) {
|
||||
e.preventDefault();
|
||||
$('#connection-button').button('loading');
|
||||
|
||||
var config = this.buildConfig();
|
||||
|
||||
Client.testEmail(
|
||||
config,
|
||||
() => {
|
||||
this.setState({
|
||||
sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
|
||||
serverError: null,
|
||||
saveNeeded: true,
|
||||
emailSuccess: true,
|
||||
emailFail: null
|
||||
});
|
||||
$('#connection-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
|
||||
serverError: null,
|
||||
saveNeeded: true,
|
||||
emailSuccess: null,
|
||||
emailFail: err.message + ' - ' + err.detailed_error
|
||||
});
|
||||
$('#connection-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('#save-button').button('loading');
|
||||
|
||||
var config = this.buildConfig();
|
||||
|
||||
Client.saveConfig(
|
||||
config,
|
||||
() => {
|
||||
AsyncClient.getConfig();
|
||||
this.setState({
|
||||
sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
|
||||
serverError: null,
|
||||
saveNeeded: false,
|
||||
emailSuccess: null,
|
||||
emailFail: null
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
sendEmailNotifications: config.EmailSettings.SendEmailNotifications,
|
||||
serverError: err.message,
|
||||
saveNeeded: true,
|
||||
emailSuccess: null,
|
||||
emailFail: null
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var saveClass = 'btn';
|
||||
if (this.state.saveNeeded) {
|
||||
saveClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
var emailSuccess = '';
|
||||
if (this.state.emailSuccess) {
|
||||
emailSuccess = (
|
||||
<div className='alert alert-success'>
|
||||
<i className='fa fa-check'></i>
|
||||
<FormattedMessage
|
||||
id='admin.email.emailSuccess'
|
||||
defaultMessage='No errors were reported while sending an email. Please check your inbox to make sure.'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var emailFail = '';
|
||||
if (this.state.emailFail) {
|
||||
emailSuccess = (
|
||||
<div className='alert alert-warning'>
|
||||
<i className='fa fa-warning'></i>
|
||||
<FormattedMessage
|
||||
id='admin.email.emailFail'
|
||||
defaultMessage='Connection unsuccessful: {error}'
|
||||
values={{
|
||||
error: this.state.emailFail
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.email.emailSettings'
|
||||
defaultMessage='Email Settings'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='allowSignUpWithEmail'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.allowSignupTitle'
|
||||
defaultMessage='Allow Sign Up With Email: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='allowSignUpWithEmail'
|
||||
value='true'
|
||||
ref='allowSignUpWithEmail'
|
||||
defaultChecked={this.props.config.EmailSettings.EnableSignUpWithEmail}
|
||||
onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_true')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='allowSignUpWithEmail'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.EmailSettings.EnableSignUpWithEmail}
|
||||
onChange={this.handleChange.bind(this, 'allowSignUpWithEmail_false')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.allowSignupDescription'
|
||||
defaultMessage='When true, Mattermost allows team creation and account signup using email and password. This value should be false only when you want to limit signup to a single-sign-on service like OAuth or LDAP.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='allowSignInWithEmail'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.allowEmailSignInTitle'
|
||||
defaultMessage='Allow Sign In With Email: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='allowSignInWithEmail'
|
||||
value='true'
|
||||
ref='allowSignInWithEmail'
|
||||
defaultChecked={this.props.config.EmailSettings.EnableSignInWithEmail}
|
||||
onChange={this.handleChange.bind(this, 'allowSignInWithEmail_true')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='allowSignInWithEmail'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.EmailSettings.EnableSignInWithEmail}
|
||||
onChange={this.handleChange.bind(this, 'allowSignInWithEmail_false')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.allowEmailSignInDescription'
|
||||
defaultMessage='When true, Mattermost allows users to sign in using their email and password.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='allowSignInWithUsername'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.allowUsernameSignInTitle'
|
||||
defaultMessage='Allow Sign In With Username: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='allowSignInWithUsername'
|
||||
value='true'
|
||||
ref='allowSignInWithUsername'
|
||||
defaultChecked={this.props.config.EmailSettings.EnableSignInWithUsername}
|
||||
onChange={this.handleChange.bind(this, 'allowSignInWithUsername_true')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='allowSignInWithUsername'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.EmailSettings.EnableSignInWithUsername}
|
||||
onChange={this.handleChange.bind(this, 'allowSignInWithUsername_false')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.allowUsernameSignInDescription'
|
||||
defaultMessage='When true, Mattermost allows users to sign in using their username and password. This setting is typically only used when email verification is disabled.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='sendEmailNotifications'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.notificationsTitle'
|
||||
defaultMessage='Send Email Notifications: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='sendEmailNotifications'
|
||||
value='true'
|
||||
ref='sendEmailNotifications'
|
||||
defaultChecked={this.props.config.EmailSettings.SendEmailNotifications}
|
||||
onChange={this.handleChange.bind(this, 'sendEmailNotifications_true')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='sendEmailNotifications'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.EmailSettings.SendEmailNotifications}
|
||||
onChange={this.handleChange.bind(this, 'sendEmailNotifications_false')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedHTMLMessage
|
||||
id='admin.email.notificationsDescription'
|
||||
defaultMessage='Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='requireEmailVerification'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.requireVerificationTitle'
|
||||
defaultMessage='Require Email Verification: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='requireEmailVerification'
|
||||
value='true'
|
||||
ref='requireEmailVerification'
|
||||
defaultChecked={this.props.config.EmailSettings.RequireEmailVerification}
|
||||
onChange={this.handleChange.bind(this, 'requireEmailVerification_true')}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='requireEmailVerification'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.EmailSettings.RequireEmailVerification}
|
||||
onChange={this.handleChange.bind(this, 'requireEmailVerification_false')}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.requireVerificationDescription'
|
||||
defaultMessage='Typically set to true in production. When true, Mattermost requires email verification after account creation prior to allowing login. Developers may set this field to false so skip sending verification emails for faster development.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='feedbackName'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.notificationDisplayTitle'
|
||||
defaultMessage='Notification Display Name:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='feedbackName'
|
||||
ref='feedbackName'
|
||||
placeholder={formatMessage(holders.notificationDisplayExample)}
|
||||
defaultValue={this.props.config.EmailSettings.FeedbackName}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.notificationDisplayDescription'
|
||||
defaultMessage='Display name on email account used when sending notification emails from Mattermost.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='feedbackEmail'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.notificationEmailTitle'
|
||||
defaultMessage='Notification Email Address:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='email'
|
||||
className='form-control'
|
||||
id='feedbackEmail'
|
||||
ref='feedbackEmail'
|
||||
placeholder={formatMessage(holders.notificationEmailExample)}
|
||||
defaultValue={this.props.config.EmailSettings.FeedbackEmail}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.notificationEmailDescription'
|
||||
defaultMessage='Email address displayed on email account used when sending notification emails from Mattermost.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='SMTPUsername'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.smtpUsernameTitle'
|
||||
defaultMessage='SMTP Username:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='SMTPUsername'
|
||||
ref='SMTPUsername'
|
||||
placeholder={formatMessage(holders.smtpUsernameExample)}
|
||||
defaultValue={this.props.config.EmailSettings.SMTPUsername}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.smtpUsernameDescription'
|
||||
defaultMessage=' Obtain this credential from administrator setting up your email server.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='SMTPPassword'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.smtpPasswordTitle'
|
||||
defaultMessage='SMTP Password:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='SMTPPassword'
|
||||
ref='SMTPPassword'
|
||||
placeholder={formatMessage(holders.smtpPasswordExample)}
|
||||
defaultValue={this.props.config.EmailSettings.SMTPPassword}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.smtpPasswordDescription'
|
||||
defaultMessage=' Obtain this credential from administrator setting up your email server.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='SMTPServer'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.smtpServerTitle'
|
||||
defaultMessage='SMTP Server:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='SMTPServer'
|
||||
ref='SMTPServer'
|
||||
placeholder={formatMessage(holders.smtpServerExample)}
|
||||
defaultValue={this.props.config.EmailSettings.SMTPServer}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.smtpServerDescription'
|
||||
defaultMessage='Location of SMTP email server.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='SMTPPort'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.smtpPortTitle'
|
||||
defaultMessage='SMTP Port:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='SMTPPort'
|
||||
ref='SMTPPort'
|
||||
placeholder={formatMessage(holders.smtpPortExample)}
|
||||
defaultValue={this.props.config.EmailSettings.SMTPPort}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.smtpPortDescription'
|
||||
defaultMessage='Port of SMTP email server.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='ConnectionSecurity'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.connectionSecurityTitle'
|
||||
defaultMessage='Connection Security:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<select
|
||||
className='form-control'
|
||||
id='ConnectionSecurity'
|
||||
ref='ConnectionSecurity'
|
||||
defaultValue={this.props.config.EmailSettings.ConnectionSecurity}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
>
|
||||
<option value=''>{formatMessage(holders.connectionSecurityNone)}</option>
|
||||
<option value='TLS'>{formatMessage(holders.connectionSecurityTls)}</option>
|
||||
<option value='STARTTLS'>{formatMessage(holders.connectionSecurityStart)}</option>
|
||||
</select>
|
||||
<div className='help-text'>
|
||||
<table
|
||||
className='table table-bordered'
|
||||
cellPadding='5'
|
||||
>
|
||||
<tbody>
|
||||
<tr><td className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.connectionSecurityNone'
|
||||
defaultMessage='None'
|
||||
/>
|
||||
</td><td className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.connectionSecurityNoneDescription'
|
||||
defaultMessage='Mattermost will send email over an unsecure connection.'
|
||||
/>
|
||||
</td></tr>
|
||||
<tr><td className='help-text'>{'TLS'}</td><td className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.connectionSecurityTlsDescription'
|
||||
defaultMessage='Encrypts the communication between Mattermost and your email server.'
|
||||
/>
|
||||
</td></tr>
|
||||
<tr><td className='help-text'>{'STARTTLS'}</td><td className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.connectionSecurityStartDescription'
|
||||
defaultMessage='Takes an existing insecure connection and attempts to upgrade it to a secure connection using TLS.'
|
||||
/>
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className='help-text'>
|
||||
<button
|
||||
className='btn btn-default'
|
||||
onClick={this.handleTestConnection}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
id='connection-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.testing)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.connectionSecurityTest'
|
||||
defaultMessage='Test Connection'
|
||||
/>
|
||||
</button>
|
||||
{emailSuccess}
|
||||
{emailFail}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='InviteSalt'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.inviteSaltTitle'
|
||||
defaultMessage='Invite Salt:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='InviteSalt'
|
||||
ref='InviteSalt'
|
||||
placeholder={formatMessage(holders.inviteSaltExample)}
|
||||
defaultValue={this.props.config.EmailSettings.InviteSalt}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.inviteSaltDescription'
|
||||
defaultMessage='32-character salt added to signing of email invites. Randomly generated on install. Click "Re-Generate" to create new salt.'
|
||||
/>
|
||||
</p>
|
||||
<div className='help-text'>
|
||||
<button
|
||||
className='btn btn-default'
|
||||
onClick={this.handleGenerateInvite}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.regenerate'
|
||||
defaultMessage='Re-Generate'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='PasswordResetSalt'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.passwordSaltTitle'
|
||||
defaultMessage='Password Reset Salt:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='PasswordResetSalt'
|
||||
ref='PasswordResetSalt'
|
||||
placeholder={formatMessage(holders.passwordSaltExample)}
|
||||
defaultValue={this.props.config.EmailSettings.PasswordResetSalt}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.passwordSaltDescription'
|
||||
defaultMessage='32-character salt added to signing of password reset emails. Randomly generated on install. Click "Re-Generate" to create new salt.'
|
||||
/>
|
||||
</p>
|
||||
<div className='help-text'>
|
||||
<button
|
||||
className='btn btn-default'
|
||||
onClick={this.handleGenerateReset}
|
||||
disabled={!this.state.sendEmailNotifications}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.regenerate'
|
||||
defaultMessage='Re-Generate'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='sendPushNotifications'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.pushTitle'
|
||||
defaultMessage='Send Push Notifications: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='sendPushNotifications'
|
||||
value='true'
|
||||
ref='sendPushNotifications'
|
||||
defaultChecked={this.props.config.EmailSettings.SendPushNotifications}
|
||||
onChange={this.handleChange.bind(this, 'sendPushNotifications_true')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='sendPushNotifications'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.EmailSettings.SendPushNotifications}
|
||||
onChange={this.handleChange.bind(this, 'sendPushNotifications_false')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.email.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.pushDesc'
|
||||
defaultMessage='Typically set to true in production. When true, Mattermost attempts to send iOS and Android push notifications through the push notification server.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='PushNotificationServer'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.pushServerTitle'
|
||||
defaultMessage='Push Notification Server:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='PushNotificationServer'
|
||||
ref='PushNotificationServer'
|
||||
placeholder={formatMessage(holders.pushServerEx)}
|
||||
defaultValue={this.props.config.EmailSettings.PushNotificationServer}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.sendPushNotifications}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.email.pushServerDesc'
|
||||
defaultMessage='Location of Mattermost push notification service you can set up behind your firewall using https://github.com/mattermost/push-proxy. For testing you can use http://push-test.mattermost.com, which connects to the sample Mattermost iOS app in the public Apple AppStore. Please do not use test service for production deployments.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
{serverError}
|
||||
<button
|
||||
disabled={!this.state.saveNeeded}
|
||||
type='submit'
|
||||
className={saveClass}
|
||||
onClick={this.handleSubmit}
|
||||
id='save-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.email.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EmailSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(EmailSettings);
|
||||
383
webapp/components/admin_console/gitlab_settings.jsx
Обычный файл
383
webapp/components/admin_console/gitlab_settings.jsx
Обычный файл
@@ -0,0 +1,383 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
clientIdExample: {
|
||||
id: 'admin.gitlab.clientIdExample',
|
||||
defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
|
||||
},
|
||||
clientSecretExample: {
|
||||
id: 'admin.gitlab.clientSecretExample',
|
||||
defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
|
||||
},
|
||||
authExample: {
|
||||
id: 'admin.gitlab.authExample',
|
||||
defaultMessage: 'Ex ""'
|
||||
},
|
||||
tokenExample: {
|
||||
id: 'admin.gitlab.tokenExample',
|
||||
defaultMessage: 'Ex ""'
|
||||
},
|
||||
userExample: {
|
||||
id: 'admin.gitlab.userExample',
|
||||
defaultMessage: 'Ex ""'
|
||||
},
|
||||
saving: {
|
||||
id: 'admin.gitlab.saving',
|
||||
defaultMessage: 'Saving Config...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class GitLabSettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
Enable: this.props.config.GitLabSettings.Enable,
|
||||
saveNeeded: false,
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
handleChange(action) {
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
|
||||
if (action === 'EnableTrue') {
|
||||
s.Enable = true;
|
||||
}
|
||||
|
||||
if (action === 'EnableFalse') {
|
||||
s.Enable = false;
|
||||
}
|
||||
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('#save-button').button('loading');
|
||||
|
||||
var config = this.props.config;
|
||||
config.GitLabSettings.Enable = ReactDOM.findDOMNode(this.refs.Enable).checked;
|
||||
config.GitLabSettings.Secret = ReactDOM.findDOMNode(this.refs.Secret).value.trim();
|
||||
config.GitLabSettings.Id = ReactDOM.findDOMNode(this.refs.Id).value.trim();
|
||||
config.GitLabSettings.AuthEndpoint = ReactDOM.findDOMNode(this.refs.AuthEndpoint).value.trim();
|
||||
config.GitLabSettings.TokenEndpoint = ReactDOM.findDOMNode(this.refs.TokenEndpoint).value.trim();
|
||||
config.GitLabSettings.UserApiEndpoint = ReactDOM.findDOMNode(this.refs.UserApiEndpoint).value.trim();
|
||||
|
||||
Client.saveConfig(
|
||||
config,
|
||||
() => {
|
||||
AsyncClient.getConfig();
|
||||
this.setState({
|
||||
serverError: null,
|
||||
saveNeeded: false
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
serverError: err.message,
|
||||
saveNeeded: true
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var saveClass = 'btn';
|
||||
if (this.state.saveNeeded) {
|
||||
saveClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.settingsTitle'
|
||||
defaultMessage='GitLab Settings'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='Enable'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.enableTitle'
|
||||
defaultMessage='Enable Sign Up With GitLab: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='Enable'
|
||||
value='true'
|
||||
ref='Enable'
|
||||
defaultChecked={this.props.config.GitLabSettings.Enable}
|
||||
onChange={this.handleChange.bind(this, 'EnableTrue')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='Enable'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.GitLabSettings.Enable}
|
||||
onChange={this.handleChange.bind(this, 'EnableFalse')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.enableDescription'
|
||||
defaultMessage='When true, Mattermost allows team creation and account signup using GitLab OAuth.'
|
||||
/>
|
||||
<br/>
|
||||
</p>
|
||||
<div className='help-text'>
|
||||
<FormattedHTMLMessage
|
||||
id='admin.gitlab.EnableHtmlDesc'
|
||||
defaultMessage='<ol><li>Log in to your GitLab account and go to Profile Settings -> Applications.</li><li>Enter Redirect URIs "<your-mattermost-url>/login/gitlab/complete" (example: http://localhost:8065/login/gitlab/complete) and "<your-mattermost-url>/signup/gitlab/complete". </li><li>Then use "Secret" and "Id" fields from GitLab to complete the options below.</li><li>Complete the Endpoint URLs below. </li></ol>'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='Id'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.clientIdTitle'
|
||||
defaultMessage='Id:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='Id'
|
||||
ref='Id'
|
||||
placeholder={formatMessage(holders.clientIdExample)}
|
||||
defaultValue={this.props.config.GitLabSettings.Id}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.Enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.clientIdDescription'
|
||||
defaultMessage='Obtain this value via the instructions above for logging into GitLab'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='Secret'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.clientSecretTitle'
|
||||
defaultMessage='Secret:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='Secret'
|
||||
ref='Secret'
|
||||
placeholder={formatMessage(holders.clientSecretExample)}
|
||||
defaultValue={this.props.config.GitLabSettings.Secret}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.Enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.gitab.clientSecretDescription'
|
||||
defaultMessage='Obtain this value via the instructions above for logging into GitLab.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='AuthEndpoint'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.authTitle'
|
||||
defaultMessage='Auth Endpoint:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='AuthEndpoint'
|
||||
ref='AuthEndpoint'
|
||||
placeholder={formatMessage(holders.authExample)}
|
||||
defaultValue={this.props.config.GitLabSettings.AuthEndpoint}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.Enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.authDescription'
|
||||
defaultMessage='Enter https://<your-gitlab-url>/oauth/authorize (example https://example.com:3000/oauth/authorize). Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='TokenEndpoint'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.tokenTitle'
|
||||
defaultMessage='Token Endpoint:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='TokenEndpoint'
|
||||
ref='TokenEndpoint'
|
||||
placeholder={formatMessage(holders.tokenExample)}
|
||||
defaultValue={this.props.config.GitLabSettings.TokenEndpoint}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.Enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.tokenDescription'
|
||||
defaultMessage='Enter https://<your-gitlab-url>/oauth/token. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='UserApiEndpoint'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.userTitle'
|
||||
defaultMessage='User API Endpoint:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='UserApiEndpoint'
|
||||
ref='UserApiEndpoint'
|
||||
placeholder={formatMessage(holders.userExample)}
|
||||
defaultValue={this.props.config.GitLabSettings.UserApiEndpoint}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.Enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.userDescription'
|
||||
defaultMessage='Enter https://<your-gitlab-url>/api/v3/user. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
{serverError}
|
||||
<button
|
||||
disabled={!this.state.saveNeeded}
|
||||
type='submit'
|
||||
className={saveClass}
|
||||
onClick={this.handleSubmit}
|
||||
id='save-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.gitlab.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
//config.GitLabSettings.Scope = ReactDOM.findDOMNode(this.refs.Scope).value.trim();
|
||||
// <div className='form-group'>
|
||||
// <label
|
||||
// className='control-label col-sm-4'
|
||||
// htmlFor='Scope'
|
||||
// >
|
||||
// {'Scope:'}
|
||||
// </label>
|
||||
// <div className='col-sm-8'>
|
||||
// <input
|
||||
// type='text'
|
||||
// className='form-control'
|
||||
// id='Scope'
|
||||
// ref='Scope'
|
||||
// placeholder='Not currently used by GitLab. Please leave blank'
|
||||
// defaultValue={this.props.config.GitLabSettings.Scope}
|
||||
// onChange={this.handleChange}
|
||||
// disabled={!this.state.Allow}
|
||||
// />
|
||||
// <p className='help-text'>{'This field is not yet used by GitLab OAuth. Other OAuth providers may use this field to specify the scope of account data from OAuth provider that is sent to Mattermost.'}</p>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
GitLabSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(GitLabSettings);
|
||||
692
webapp/components/admin_console/image_settings.jsx
Обычный файл
692
webapp/components/admin_console/image_settings.jsx
Обычный файл
@@ -0,0 +1,692 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import crypto from 'crypto';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
storeLocal: {
|
||||
id: 'admin.image.storeLocal',
|
||||
defaultMessage: 'Local File System'
|
||||
},
|
||||
storeAmazonS3: {
|
||||
id: 'admin.image.storeAmazonS3',
|
||||
defaultMessage: 'Amazon S3'
|
||||
},
|
||||
localExample: {
|
||||
id: 'admin.image.localExample',
|
||||
defaultMessage: 'Ex "./data/"'
|
||||
},
|
||||
amazonS3IdExample: {
|
||||
id: 'admin.image.amazonS3IdExample',
|
||||
defaultMessage: 'Ex "AKIADTOVBGERKLCBV"'
|
||||
},
|
||||
amazonS3SecretExample: {
|
||||
id: 'admin.image.amazonS3SecretExample',
|
||||
defaultMessage: 'Ex "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'
|
||||
},
|
||||
amazonS3BucketExample: {
|
||||
id: 'admin.image.amazonS3BucketExample',
|
||||
defaultMessage: 'Ex "mattermost-media"'
|
||||
},
|
||||
amazonS3RegionExample: {
|
||||
id: 'admin.image.amazonS3RegionExample',
|
||||
defaultMessage: 'Ex "us-east-1"'
|
||||
},
|
||||
thumbWidthExample: {
|
||||
id: 'admin.image.thumbWidthExample',
|
||||
defaultMessage: 'Ex "120"'
|
||||
},
|
||||
thumbHeightExample: {
|
||||
id: 'admin.image.thumbHeightExample',
|
||||
defaultMessage: 'Ex "100"'
|
||||
},
|
||||
previewWidthExample: {
|
||||
id: 'admin.image.previewWidthExample',
|
||||
defaultMessage: 'Ex "1024"'
|
||||
},
|
||||
previewHeightExample: {
|
||||
id: 'admin.image.previewHeightExample',
|
||||
defaultMessage: 'Ex "0"'
|
||||
},
|
||||
profileWidthExample: {
|
||||
id: 'admin.image.profileWidthExample',
|
||||
defaultMessage: 'Ex "1024"'
|
||||
},
|
||||
profileHeightExample: {
|
||||
id: 'admin.image.profileHeightExample',
|
||||
defaultMessage: 'Ex "0"'
|
||||
},
|
||||
publicLinkExample: {
|
||||
id: 'admin.image.publicLinkExample',
|
||||
defaultMessage: 'Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
|
||||
},
|
||||
saving: {
|
||||
id: 'admin.image.saving',
|
||||
defaultMessage: 'Saving Config...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class FileSettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleGenerate = this.handleGenerate.bind(this);
|
||||
|
||||
this.state = {
|
||||
saveNeeded: false,
|
||||
serverError: null,
|
||||
DriverName: this.props.config.FileSettings.DriverName
|
||||
};
|
||||
}
|
||||
|
||||
handleChange(action) {
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
|
||||
if (action === 'DriverName') {
|
||||
s.DriverName = ReactDOM.findDOMNode(this.refs.DriverName).value;
|
||||
}
|
||||
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleGenerate(e) {
|
||||
e.preventDefault();
|
||||
ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('#save-button').button('loading');
|
||||
|
||||
var config = this.props.config;
|
||||
config.FileSettings.DriverName = ReactDOM.findDOMNode(this.refs.DriverName).value;
|
||||
config.FileSettings.Directory = ReactDOM.findDOMNode(this.refs.Directory).value;
|
||||
config.FileSettings.AmazonS3AccessKeyId = ReactDOM.findDOMNode(this.refs.AmazonS3AccessKeyId).value;
|
||||
config.FileSettings.AmazonS3SecretAccessKey = ReactDOM.findDOMNode(this.refs.AmazonS3SecretAccessKey).value;
|
||||
config.FileSettings.AmazonS3Bucket = ReactDOM.findDOMNode(this.refs.AmazonS3Bucket).value;
|
||||
config.FileSettings.AmazonS3Region = ReactDOM.findDOMNode(this.refs.AmazonS3Region).value;
|
||||
config.FileSettings.EnablePublicLink = ReactDOM.findDOMNode(this.refs.EnablePublicLink).checked;
|
||||
|
||||
config.FileSettings.PublicLinkSalt = ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value.trim();
|
||||
|
||||
if (config.FileSettings.PublicLinkSalt === '') {
|
||||
config.FileSettings.PublicLinkSalt = crypto.randomBytes(256).toString('base64').substring(0, 32);
|
||||
ReactDOM.findDOMNode(this.refs.PublicLinkSalt).value = config.FileSettings.PublicLinkSalt;
|
||||
}
|
||||
|
||||
var thumbnailWidth = 120;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value, 10))) {
|
||||
thumbnailWidth = parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value, 10);
|
||||
}
|
||||
config.FileSettings.ThumbnailWidth = thumbnailWidth;
|
||||
ReactDOM.findDOMNode(this.refs.ThumbnailWidth).value = thumbnailWidth;
|
||||
|
||||
var thumbnailHeight = 100;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value, 10))) {
|
||||
thumbnailHeight = parseInt(ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value, 10);
|
||||
}
|
||||
config.FileSettings.ThumbnailHeight = thumbnailHeight;
|
||||
ReactDOM.findDOMNode(this.refs.ThumbnailHeight).value = thumbnailHeight;
|
||||
|
||||
var previewWidth = 1024;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PreviewWidth).value, 10))) {
|
||||
previewWidth = parseInt(ReactDOM.findDOMNode(this.refs.PreviewWidth).value, 10);
|
||||
}
|
||||
config.FileSettings.PreviewWidth = previewWidth;
|
||||
ReactDOM.findDOMNode(this.refs.PreviewWidth).value = previewWidth;
|
||||
|
||||
var previewHeight = 0;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PreviewHeight).value, 10))) {
|
||||
previewHeight = parseInt(ReactDOM.findDOMNode(this.refs.PreviewHeight).value, 10);
|
||||
}
|
||||
config.FileSettings.PreviewHeight = previewHeight;
|
||||
ReactDOM.findDOMNode(this.refs.PreviewHeight).value = previewHeight;
|
||||
|
||||
var profileWidth = 128;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ProfileWidth).value, 10))) {
|
||||
profileWidth = parseInt(ReactDOM.findDOMNode(this.refs.ProfileWidth).value, 10);
|
||||
}
|
||||
config.FileSettings.ProfileWidth = profileWidth;
|
||||
ReactDOM.findDOMNode(this.refs.ProfileWidth).value = profileWidth;
|
||||
|
||||
var profileHeight = 128;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.ProfileHeight).value, 10))) {
|
||||
profileHeight = parseInt(ReactDOM.findDOMNode(this.refs.ProfileHeight).value, 10);
|
||||
}
|
||||
config.FileSettings.ProfileHeight = profileHeight;
|
||||
ReactDOM.findDOMNode(this.refs.ProfileHeight).value = profileHeight;
|
||||
|
||||
Client.saveConfig(
|
||||
config,
|
||||
() => {
|
||||
AsyncClient.getConfig();
|
||||
this.setState({
|
||||
serverError: null,
|
||||
saveNeeded: false
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
serverError: err.message,
|
||||
saveNeeded: true
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var saveClass = 'btn';
|
||||
if (this.state.saveNeeded) {
|
||||
saveClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
var enableFile = false;
|
||||
var enableS3 = false;
|
||||
|
||||
if (this.state.DriverName === 'local') {
|
||||
enableFile = true;
|
||||
}
|
||||
|
||||
if (this.state.DriverName === 'amazons3') {
|
||||
enableS3 = true;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.image.fileSettings'
|
||||
defaultMessage='File Settings'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='DriverName'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.storeTitle'
|
||||
defaultMessage='Store Files In:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<select
|
||||
className='form-control'
|
||||
id='DriverName'
|
||||
ref='DriverName'
|
||||
defaultValue={this.props.config.FileSettings.DriverName}
|
||||
onChange={this.handleChange.bind(this, 'DriverName')}
|
||||
>
|
||||
<option value='local'>{formatMessage(holders.storeLocal)}</option>
|
||||
<option value='amazons3'>{formatMessage(holders.storeAmazonS3)}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='Directory'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.localTitle'
|
||||
defaultMessage='Local Directory Location:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='Directory'
|
||||
ref='Directory'
|
||||
placeholder={formatMessage(holders.localExample)}
|
||||
defaultValue={this.props.config.FileSettings.Directory}
|
||||
onChange={this.handleChange}
|
||||
disabled={!enableFile}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.localDescription'
|
||||
defaultMessage='Directory to which image files are written. If blank, will be set to ./data/.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='AmazonS3AccessKeyId'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.amazonS3IdTitle'
|
||||
defaultMessage='Amazon S3 Access Key Id:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='AmazonS3AccessKeyId'
|
||||
ref='AmazonS3AccessKeyId'
|
||||
placeholder={formatMessage(holders.amazonS3IdExample)}
|
||||
defaultValue={this.props.config.FileSettings.AmazonS3AccessKeyId}
|
||||
onChange={this.handleChange}
|
||||
disabled={!enableS3}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.amazonS3IdDescription'
|
||||
defaultMessage='Obtain this credential from your Amazon EC2 administrator.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='AmazonS3SecretAccessKey'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.amazonS3SecretTitle'
|
||||
defaultMessage='Amazon S3 Secret Access Key:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='AmazonS3SecretAccessKey'
|
||||
ref='AmazonS3SecretAccessKey'
|
||||
placeholder={formatMessage(holders.amazonS3SecretExample)}
|
||||
defaultValue={this.props.config.FileSettings.AmazonS3SecretAccessKey}
|
||||
onChange={this.handleChange}
|
||||
disabled={!enableS3}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.amazonS3SecretDescription'
|
||||
defaultMessage='Obtain this credential from your Amazon EC2 administrator.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='AmazonS3Bucket'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.amazonS3BucketTitle'
|
||||
defaultMessage='Amazon S3 Bucket:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='AmazonS3Bucket'
|
||||
ref='AmazonS3Bucket'
|
||||
placeholder={formatMessage(holders.amazonS3BucketExample)}
|
||||
defaultValue={this.props.config.FileSettings.AmazonS3Bucket}
|
||||
onChange={this.handleChange}
|
||||
disabled={!enableS3}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.amazonS3BucketDescription'
|
||||
defaultMessage='Name you selected for your S3 bucket in AWS.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='AmazonS3Region'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.amazonS3RegionTitle'
|
||||
defaultMessage='Amazon S3 Region:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='AmazonS3Region'
|
||||
ref='AmazonS3Region'
|
||||
placeholder={formatMessage(holders.amazonS3RegionExample)}
|
||||
defaultValue={this.props.config.FileSettings.AmazonS3Region}
|
||||
onChange={this.handleChange}
|
||||
disabled={!enableS3}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.amazonS3RegionDescription'
|
||||
defaultMessage='AWS region you selected for creating your S3 bucket.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='ThumbnailWidth'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.thumbWidthTitle'
|
||||
defaultMessage='Thumbnail Width:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='ThumbnailWidth'
|
||||
ref='ThumbnailWidth'
|
||||
placeholder={formatMessage(holders.thumbWidthExample)}
|
||||
defaultValue={this.props.config.FileSettings.ThumbnailWidth}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.thumbWidthDescription'
|
||||
defaultMessage='Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='ThumbnailHeight'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.thumbHeightTitle'
|
||||
defaultMessage='Thumbnail Height:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='ThumbnailHeight'
|
||||
ref='ThumbnailHeight'
|
||||
placeholder={formatMessage(holders.thumbHeightExample)}
|
||||
defaultValue={this.props.config.FileSettings.ThumbnailHeight}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.thumbHeightDescription'
|
||||
defaultMessage='Height of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='PreviewWidth'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.previewWidthTitle'
|
||||
defaultMessage='Preview Width:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='PreviewWidth'
|
||||
ref='PreviewWidth'
|
||||
placeholder={formatMessage(holders.previewWidthExample)}
|
||||
defaultValue={this.props.config.FileSettings.PreviewWidth}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.previewWidthDescription'
|
||||
defaultMessage='Maximum width of preview image. Updating this value changes how preview images render in future, but does not change images created in the past.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='PreviewHeight'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.previewHeightTitle'
|
||||
defaultMessage='Preview Height:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='PreviewHeight'
|
||||
ref='PreviewHeight'
|
||||
placeholder={formatMessage(holders.previewHeightExample)}
|
||||
defaultValue={this.props.config.FileSettings.PreviewHeight}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.previewHeightDescription'
|
||||
defaultMessage='Maximum height of preview image ("0": Sets to auto-size). Updating this value changes how preview images render in future, but does not change images created in the past.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='ProfileWidth'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.profileWidthTitle'
|
||||
defaultMessage='Profile Width:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='ProfileWidth'
|
||||
ref='ProfileWidth'
|
||||
placeholder={formatMessage(holders.profileWidthExample)}
|
||||
defaultValue={this.props.config.FileSettings.ProfileWidth}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.profileWidthDescription'
|
||||
defaultMessage='Width of profile picture.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='ProfileHeight'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.profileHeightTitle'
|
||||
defaultMessage='Profile Height:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='ProfileHeight'
|
||||
ref='ProfileHeight'
|
||||
placeholder={formatMessage(holders.profileHeightExample)}
|
||||
defaultValue={this.props.config.FileSettings.ProfileHeight}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.profileHeightDescription'
|
||||
defaultMessage='Height of profile picture.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnablePublicLink'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.shareTitle'
|
||||
defaultMessage='Share Public File Link: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnablePublicLink'
|
||||
value='true'
|
||||
ref='EnablePublicLink'
|
||||
defaultChecked={this.props.config.FileSettings.EnablePublicLink}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.image.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnablePublicLink'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.FileSettings.EnablePublicLink}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.image.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.shareDescription'
|
||||
defaultMessage='Allow users to share public links to files and images.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='PublicLinkSalt'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.publicLinkTitle'
|
||||
defaultMessage='Public Link Salt:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='PublicLinkSalt'
|
||||
ref='PublicLinkSalt'
|
||||
placeholder={formatMessage(holders.publicLinkExample)}
|
||||
defaultValue={this.props.config.FileSettings.PublicLinkSalt}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.image.publicLinkDescription'
|
||||
defaultMessage='32-character salt added to signing of public image links. Randomly generated on install. Click "Re-Generate" to create new salt.'
|
||||
/>
|
||||
</p>
|
||||
<div className='help-text'>
|
||||
<button
|
||||
className='btn btn-default'
|
||||
onClick={this.handleGenerate}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.regenerate'
|
||||
defaultMessage='Re-Generate'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
{serverError}
|
||||
<button
|
||||
disabled={!this.state.saveNeeded}
|
||||
type='submit'
|
||||
className={saveClass}
|
||||
onClick={this.handleSubmit}
|
||||
id='save-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.image.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(FileSettings);
|
||||
588
webapp/components/admin_console/ldap_settings.jsx
Обычный файл
588
webapp/components/admin_console/ldap_settings.jsx
Обычный файл
@@ -0,0 +1,588 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||
|
||||
const DEFAULT_LDAP_PORT = 389;
|
||||
const DEFAULT_QUERY_TIMEOUT = 60;
|
||||
|
||||
var holders = defineMessages({
|
||||
serverEx: {
|
||||
id: 'admin.ldap.serverEx',
|
||||
defaultMessage: 'Ex "10.0.0.23"'
|
||||
},
|
||||
portEx: {
|
||||
id: 'admin.ldap.portEx',
|
||||
defaultMessage: 'Ex "389"'
|
||||
},
|
||||
baseEx: {
|
||||
id: 'admin.ldap.baseEx',
|
||||
defaultMessage: 'Ex "ou=Unit Name,dc=corp,dc=example,dc=com"'
|
||||
},
|
||||
firstnameAttrEx: {
|
||||
id: 'admin.ldap.firstnameAttrEx',
|
||||
defaultMessage: 'Ex "givenName"'
|
||||
},
|
||||
lastnameAttrEx: {
|
||||
id: 'admin.ldap.lastnameAttrEx',
|
||||
defaultMessage: 'Ex "sn"'
|
||||
},
|
||||
emailAttrEx: {
|
||||
id: 'admin.ldap.emailAttrEx',
|
||||
defaultMessage: 'Ex "mail" or "userPrincipalName"'
|
||||
},
|
||||
usernameAttrEx: {
|
||||
id: 'admin.ldap.usernameAttrEx',
|
||||
defaultMessage: 'Ex "sAMAccountName"'
|
||||
},
|
||||
idAttrEx: {
|
||||
id: 'admin.ldap.idAttrEx',
|
||||
defaultMessage: 'Ex "sAMAccountName"'
|
||||
},
|
||||
queryEx: {
|
||||
id: 'admin.ldap.queryEx',
|
||||
defaultMessage: 'Ex "60"'
|
||||
},
|
||||
saving: {
|
||||
id: 'admin.ldap.saving',
|
||||
defaultMessage: 'Saving Config...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class LdapSettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleEnable = this.handleEnable.bind(this);
|
||||
this.handleDisable = this.handleDisable.bind(this);
|
||||
|
||||
this.state = {
|
||||
saveNeeded: false,
|
||||
serverError: null,
|
||||
enable: this.props.config.LdapSettings.Enable
|
||||
};
|
||||
}
|
||||
handleChange() {
|
||||
this.setState({saveNeeded: true});
|
||||
}
|
||||
handleEnable() {
|
||||
this.setState({saveNeeded: true, enable: true});
|
||||
}
|
||||
handleDisable() {
|
||||
this.setState({saveNeeded: true, enable: false});
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('#save-button').button('loading');
|
||||
|
||||
const config = this.props.config;
|
||||
config.LdapSettings.Enable = this.refs.Enable.checked;
|
||||
config.LdapSettings.LdapServer = this.refs.LdapServer.value.trim();
|
||||
|
||||
let LdapPort = DEFAULT_LDAP_PORT;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.LdapPort).value, 10))) {
|
||||
LdapPort = parseInt(ReactDOM.findDOMNode(this.refs.LdapPort).value, 10);
|
||||
}
|
||||
config.LdapSettings.LdapPort = LdapPort;
|
||||
|
||||
config.LdapSettings.BaseDN = this.refs.BaseDN.value.trim();
|
||||
config.LdapSettings.BindUsername = this.refs.BindUsername.value.trim();
|
||||
config.LdapSettings.BindPassword = this.refs.BindPassword.value.trim();
|
||||
config.LdapSettings.FirstNameAttribute = this.refs.FirstNameAttribute.value.trim();
|
||||
config.LdapSettings.LastNameAttribute = this.refs.LastNameAttribute.value.trim();
|
||||
config.LdapSettings.EmailAttribute = this.refs.EmailAttribute.value.trim();
|
||||
config.LdapSettings.UsernameAttribute = this.refs.UsernameAttribute.value.trim();
|
||||
config.LdapSettings.IdAttribute = this.refs.IdAttribute.value.trim();
|
||||
|
||||
let QueryTimeout = DEFAULT_QUERY_TIMEOUT;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.QueryTimeout).value, 10))) {
|
||||
QueryTimeout = parseInt(ReactDOM.findDOMNode(this.refs.QueryTimeout).value, 10);
|
||||
}
|
||||
config.LdapSettings.QueryTimeout = QueryTimeout;
|
||||
|
||||
Client.saveConfig(
|
||||
config,
|
||||
() => {
|
||||
AsyncClient.getConfig();
|
||||
this.setState({
|
||||
serverError: null,
|
||||
saveNeeded: false
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
serverError: err.message,
|
||||
saveNeeded: true
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
let serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
let saveClass = 'btn';
|
||||
if (this.state.saveNeeded) {
|
||||
saveClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true';
|
||||
|
||||
let bannerContent;
|
||||
if (licenseEnabled) {
|
||||
bannerContent = (
|
||||
<div className='banner'>
|
||||
<div className='banner__content'>
|
||||
<h4 className='banner__heading'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.bannerHeading'
|
||||
defaultMessage='Note:'
|
||||
/>
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.bannerDesc'
|
||||
defaultMessage='If a user attribute changes on the LDAP server it will be updated the next time the user enters their credentials to log in to Mattermost. This includes if a user is made inactive or removed from an LDAP server. Synchronization with LDAP servers is planned in a future release.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
bannerContent = (
|
||||
<div className='banner warning'>
|
||||
<div className='banner__content'>
|
||||
<FormattedHTMLMessage
|
||||
id='admin.ldap.noLicense'
|
||||
defaultMessage='<h4 class="banner__heading">Note:</h4><p>LDAP is an enterprise feature. Your current license does not support LDAP. Click <a href="http://mattermost.com"target="_blank">here</a> for information and pricing on enterprise licenses.</p>'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
{bannerContent}
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.title'
|
||||
defaultMessage='LDAP Settings'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='Enable'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.enableTitle'
|
||||
defaultMessage='Enable Login With LDAP:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='Enable'
|
||||
value='true'
|
||||
ref='Enable'
|
||||
defaultChecked={this.props.config.LdapSettings.Enable}
|
||||
onChange={this.handleEnable}
|
||||
disabled={!licenseEnabled}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='Enable'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.LdapSettings.Enable}
|
||||
onChange={this.handleDisable}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.enableDesc'
|
||||
defaultMessage='When true, Mattermost allows login using LDAP'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='LdapServer'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.serverTitle'
|
||||
defaultMessage='LDAP Server:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='LdapServer'
|
||||
ref='LdapServer'
|
||||
placeholder={formatMessage(holders.serverEx)}
|
||||
defaultValue={this.props.config.LdapSettings.LdapServer}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.serverDesc'
|
||||
defaultMessage='The domain or IP address of LDAP server.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='LdapPort'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.portTitle'
|
||||
defaultMessage='LDAP Port:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='number'
|
||||
className='form-control'
|
||||
id='LdapPort'
|
||||
ref='LdapPort'
|
||||
placeholder={formatMessage(holders.portEx)}
|
||||
defaultValue={this.props.config.LdapSettings.LdapPort}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.portDesc'
|
||||
defaultMessage='The port Mattermost will use to connect to the LDAP server. Default is 389.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='BaseDN'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.baseTitle'
|
||||
defaultMessage='BaseDN:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='BaseDN'
|
||||
ref='BaseDN'
|
||||
placeholder={formatMessage(holders.baseEx)}
|
||||
defaultValue={this.props.config.LdapSettings.BaseDN}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.baseDesc'
|
||||
defaultMessage='The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='BindUsername'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.bindUserTitle'
|
||||
defaultMessage='Bind Username:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='BindUsername'
|
||||
ref='BindUsername'
|
||||
placeholder=''
|
||||
defaultValue={this.props.config.LdapSettings.BindUsername}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.bindUserDesc'
|
||||
defaultMessage='The username used to perform the LDAP search. This should typically be an account created specifically for use with Mattermost. It should have access limited to read the portion of the LDAP tree specified in the BaseDN field.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='BindPassword'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.bindPwdTitle'
|
||||
defaultMessage='Bind Password:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='password'
|
||||
className='form-control'
|
||||
id='BindPassword'
|
||||
ref='BindPassword'
|
||||
placeholder=''
|
||||
defaultValue={this.props.config.LdapSettings.BindPassword}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.bindPwdDesc'
|
||||
defaultMessage='Password of the user given in "Bind Username".'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='FirstNameAttribute'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.firstnameAttrTitle'
|
||||
defaultMessage='First Name Attrubute'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='FirstNameAttribute'
|
||||
ref='FirstNameAttribute'
|
||||
placeholder={formatMessage(holders.firstnameAttrEx)}
|
||||
defaultValue={this.props.config.LdapSettings.FirstNameAttribute}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.firstnameAttrDesc'
|
||||
defaultMessage='The attribute in the LDAP server that will be used to populate the first name of users in Mattermost.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='LastNameAttribute'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.lastnameAttrTitle'
|
||||
defaultMessage='Last Name Attribute:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='LastNameAttribute'
|
||||
ref='LastNameAttribute'
|
||||
placeholder={formatMessage(holders.lastnameAttrEx)}
|
||||
defaultValue={this.props.config.LdapSettings.LastNameAttribute}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.lastnameAttrDesc'
|
||||
defaultMessage='The attribute in the LDAP server that will be used to populate the last name of users in Mattermost.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EmailAttribute'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.emailAttrTitle'
|
||||
defaultMessage='Email Attribute:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='EmailAttribute'
|
||||
ref='EmailAttribute'
|
||||
placeholder={formatMessage(holders.emailAttrEx)}
|
||||
defaultValue={this.props.config.LdapSettings.EmailAttribute}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.emailAttrDesc'
|
||||
defaultMessage='The attribute in the LDAP server that will be used to populate the email addresses of users in Mattermost.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='UsernameAttribute'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.usernameAttrTitle'
|
||||
defaultMessage='Username Attribute:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='UsernameAttribute'
|
||||
ref='UsernameAttribute'
|
||||
placeholder={formatMessage(holders.usernameAttrEx)}
|
||||
defaultValue={this.props.config.LdapSettings.UsernameAttribute}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.uernameAttrDesc'
|
||||
defaultMessage='The attribute in the LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='IdAttribute'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.idAttrTitle'
|
||||
defaultMessage='Id Attribute: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='IdAttribute'
|
||||
ref='IdAttribute'
|
||||
placeholder={formatMessage(holders.idAttrEx)}
|
||||
defaultValue={this.props.config.LdapSettings.IdAttribute}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.idAttrDesc'
|
||||
defaultMessage='The attribute in the LDAP server that will be used as a unique identifier in Mattermost. It should be an LDAP attribute with a value that does not change, such as username or uid. If a user’s Id Attribute changes, it will create a new Mattermost account unassociated with their old one. This is the value used to log in to Mattermost in the "LDAP Username" field on the sign in page. Normally this attribute is the same as the “Username Attribute” field above. If your team typically uses domain\\username to sign in to other services with LDAP, you may choose to put domain\\username in this field to maintain consistency between sites.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='QueryTimeout'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.queryTitle'
|
||||
defaultMessage='Query Timeout (seconds):'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='number'
|
||||
className='form-control'
|
||||
id='QueryTimeout'
|
||||
ref='QueryTimeout'
|
||||
placeholder={formatMessage(holders.queryEx)}
|
||||
defaultValue={this.props.config.LdapSettings.QueryTimeout}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.enable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.queryDesc'
|
||||
defaultMessage='The timeout value for queries to the LDAP server. Increase if you are getting timeout errors caused by a slow LDAP server.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
{serverError}
|
||||
<button
|
||||
disabled={!this.state.saveNeeded}
|
||||
type='submit'
|
||||
className={saveClass}
|
||||
onClick={this.handleSubmit}
|
||||
id='save-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.ldap.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
LdapSettings.defaultProps = {
|
||||
};
|
||||
|
||||
LdapSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(LdapSettings);
|
||||
294
webapp/components/admin_console/legal_and_support_settings.jsx
Обычный файл
294
webapp/components/admin_console/legal_and_support_settings.jsx
Обычный файл
@@ -0,0 +1,294 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
var holders = defineMessages({
|
||||
saving: {
|
||||
id: 'admin.support.saving',
|
||||
defaultMessage: 'Saving Config...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class LegalAndSupportSettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
saveNeeded: false,
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('#save-button').button('loading');
|
||||
|
||||
var config = this.props.config;
|
||||
|
||||
config.SupportSettings.TermsOfServiceLink = ReactDOM.findDOMNode(this.refs.TermsOfServiceLink).value.trim();
|
||||
config.SupportSettings.PrivacyPolicyLink = ReactDOM.findDOMNode(this.refs.PrivacyPolicyLink).value.trim();
|
||||
config.SupportSettings.AboutLink = ReactDOM.findDOMNode(this.refs.AboutLink).value.trim();
|
||||
config.SupportSettings.HelpLink = ReactDOM.findDOMNode(this.refs.HelpLink).value.trim();
|
||||
config.SupportSettings.ReportAProblemLink = ReactDOM.findDOMNode(this.refs.ReportAProblemLink).value.trim();
|
||||
config.SupportSettings.SupportEmail = ReactDOM.findDOMNode(this.refs.SupportEmail).value.trim();
|
||||
|
||||
Client.saveConfig(
|
||||
config,
|
||||
() => {
|
||||
AsyncClient.getConfig();
|
||||
this.setState({
|
||||
serverError: null,
|
||||
saveNeeded: false
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
serverError: err.message,
|
||||
saveNeeded: true
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var saveClass = 'btn';
|
||||
if (this.state.saveNeeded) {
|
||||
saveClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.support.title'
|
||||
defaultMessage='Legal and Support Settings'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='TermsOfServiceLink'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.support.termsTitle'
|
||||
defaultMessage='Terms of Service link:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='TermsOfServiceLink'
|
||||
ref='TermsOfServiceLink'
|
||||
defaultValue={this.props.config.SupportSettings.TermsOfServiceLink}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.support.termsDesc'
|
||||
defaultMessage='Link to Terms of Service available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='PrivacyPolicyLink'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.support.privacyTitle'
|
||||
defaultMessage='Privacy Policy link:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='PrivacyPolicyLink'
|
||||
ref='PrivacyPolicyLink'
|
||||
defaultValue={this.props.config.SupportSettings.PrivacyPolicyLink}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.support.privacyDesc'
|
||||
defaultMessage='Link to Privacy Policy available to users on desktop and on mobile. Leaving this blank will hide the option to display a notice.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='AboutLink'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.support.aboutTitle'
|
||||
defaultMessage='About link:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='AboutLink'
|
||||
ref='AboutLink'
|
||||
defaultValue={this.props.config.SupportSettings.AboutLink}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.support.aboutDesc'
|
||||
defaultMessage='Link to About page for more information on your Mattermost deployment, for example its purpose and audience within your organization. Defaults to Mattermost information page.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='HelpLink'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.support.helpTitle'
|
||||
defaultMessage='Help link:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='HelpLink'
|
||||
ref='HelpLink'
|
||||
defaultValue={this.props.config.SupportSettings.HelpLink}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.support.helpDesc'
|
||||
defaultMessage='Link to help documentation from team site main menu. Typically not changed unless your organization chooses to create custom documentation.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='ReportAProblemLink'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.support.problemTitle'
|
||||
defaultMessage='Report a Problem link:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='ReportAProblemLink'
|
||||
ref='ReportAProblemLink'
|
||||
defaultValue={this.props.config.SupportSettings.ReportAProblemLink}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.support.problemDesc'
|
||||
defaultMessage='Link to help documentation from team site main menu. By default this points to the peer-to-peer troubleshooting forum where users can search for, find and request help with technical issues.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='SupportEmail'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.support.emailTitle'
|
||||
defaultMessage='Support email:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='SupportEmail'
|
||||
ref='SupportEmail'
|
||||
defaultValue={this.props.config.SupportSettings.SupportEmail}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.support.emailHelp'
|
||||
defaultMessage='Email shown during tutorial for end users to ask support questions.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
{serverError}
|
||||
<button
|
||||
disabled={!this.state.saveNeeded}
|
||||
type='submit'
|
||||
className={saveClass}
|
||||
onClick={this.handleSubmit}
|
||||
id='save-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + this.props.intl.formatMessage(holders.saving)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.support.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LegalAndSupportSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(LegalAndSupportSettings);
|
||||
295
webapp/components/admin_console/license_settings.jsx
Обычный файл
295
webapp/components/admin_console/license_settings.jsx
Обычный файл
@@ -0,0 +1,295 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
removing: {
|
||||
id: 'admin.license.removing',
|
||||
defaultMessage: 'Removing License...'
|
||||
},
|
||||
uploading: {
|
||||
id: 'admin.license.uploading',
|
||||
defaultMessage: 'Uploading License...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class LicenseSettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleRemove = this.handleRemove.bind(this);
|
||||
|
||||
this.state = {
|
||||
fileSelected: false,
|
||||
fileName: null,
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
|
||||
if (element.prop('files').length > 0) {
|
||||
this.setState({fileSelected: true, fileName: element.prop('files')[0].name});
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
|
||||
if (element.prop('files').length === 0) {
|
||||
return;
|
||||
}
|
||||
const file = element.prop('files')[0];
|
||||
|
||||
$('#upload-button').button('loading');
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('license', file, file.name);
|
||||
|
||||
Client.uploadLicenseFile(formData,
|
||||
() => {
|
||||
Utils.clearFileInput(element[0]);
|
||||
$('#upload-button').button('reset');
|
||||
this.setState({fileSelected: false, fileName: null, serverError: null});
|
||||
window.location.reload(true);
|
||||
},
|
||||
(error) => {
|
||||
Utils.clearFileInput(element[0]);
|
||||
$('#upload-button').button('reset');
|
||||
this.setState({fileSelected: false, fileName: null, serverError: error.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleRemove(e) {
|
||||
e.preventDefault();
|
||||
|
||||
$('#remove-button').button('loading');
|
||||
|
||||
Client.removeLicenseFile(
|
||||
() => {
|
||||
$('#remove-button').button('reset');
|
||||
this.setState({fileSelected: false, fileName: null, serverError: null});
|
||||
window.location.reload(true);
|
||||
},
|
||||
(error) => {
|
||||
$('#remove-button').button('reset');
|
||||
this.setState({fileSelected: false, fileName: null, serverError: error.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var btnClass = 'btn';
|
||||
if (this.state.fileSelected) {
|
||||
btnClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
let edition;
|
||||
let licenseType;
|
||||
let licenseKey;
|
||||
|
||||
if (global.window.mm_license.IsLicensed === 'true') {
|
||||
edition = (
|
||||
<FormattedMessage
|
||||
id='admin.license.enterpriseEdition'
|
||||
defaultMessage='Mattermost Enterprise Edition. Designed for enterprise-scale communication.'
|
||||
/>
|
||||
);
|
||||
licenseType = (
|
||||
<FormattedHTMLMessage
|
||||
id='admin.license.enterpriseType'
|
||||
values={{
|
||||
terms: global.window.mm_config.TermsOfServiceLink,
|
||||
name: global.window.mm_license.Name,
|
||||
company: global.window.mm_license.Company,
|
||||
users: global.window.mm_license.Users,
|
||||
issued: Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10)) + ' ' + Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true),
|
||||
start: Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10)),
|
||||
expires: Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10)),
|
||||
ldap: global.window.mm_license.LDAP
|
||||
}}
|
||||
defaultMessage='<div><p>This compiled release of Mattermost platform is provided under a <a href="http://mattermost.com" target="_blank">commercial license</a> from Mattermost, Inc. based on your subscription level and is subject to the <a href="{terms}" target="_blank">Terms of Service.</a></p>
|
||||
<p>Your subscription details are as follows:</p>
|
||||
Name: {name}<br />
|
||||
Company or organization name: {company}<br/>
|
||||
Number of users: {users}<br/>
|
||||
License issued: {issued}<br/>
|
||||
Start date of license: {start}<br/>
|
||||
Expiry date of license: {expires}<br/>
|
||||
LDAP: {ldap}<br/></div>'
|
||||
/>
|
||||
);
|
||||
|
||||
licenseKey = (
|
||||
<div className='col-sm-8'>
|
||||
<button
|
||||
disabled={this.props.config.LdapSettings.Enable}
|
||||
className='btn btn-danger'
|
||||
onClick={this.handleRemove}
|
||||
id='remove-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + this.props.intl.formatMessage(holders.removing)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.license.keyRemove'
|
||||
defaultMessage='Remove Enterprise License and Downgrade Server'
|
||||
/>
|
||||
</button>
|
||||
<br/>
|
||||
<br/>
|
||||
<p className='help-text'>
|
||||
<FormattedHTMLMessage
|
||||
id='admin.licence.keyMigration'
|
||||
defaultMessage='If you’re migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, <a href="http://mattermost.com" target="_blank">disable all Enterprise Edition features on this server</a>. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
edition = (
|
||||
<FormattedMessage
|
||||
id='admin.license.teamEdition'
|
||||
defaultMessage='Mattermost Team Edition. Designed for teams from 5 to 50 users.'
|
||||
/>
|
||||
);
|
||||
|
||||
licenseType = (
|
||||
<FormattedHTMLMessage
|
||||
id='admin.license.teamType'
|
||||
defaultMessage='<span><p>This compiled release of Mattermost platform is offered under an MIT license.</p>
|
||||
<p>See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.</p></span>'
|
||||
/>
|
||||
);
|
||||
|
||||
let fileName;
|
||||
if (this.state.fileName) {
|
||||
fileName = this.state.fileName;
|
||||
} else {
|
||||
fileName = (
|
||||
<FormattedMessage
|
||||
id='admin.license.noFile'
|
||||
defaultMessage='No file uploaded'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
licenseKey = (
|
||||
<div className='col-sm-8'>
|
||||
<div className='file__upload'>
|
||||
<button className='btn btn-default'>
|
||||
<FormattedMessage
|
||||
id='admin.license.choose'
|
||||
defaultMessage='Choose File'
|
||||
/>
|
||||
</button>
|
||||
<input
|
||||
ref='fileInput'
|
||||
type='file'
|
||||
accept='.mattermost-license'
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className={btnClass}
|
||||
disabled={!this.state.fileSelected}
|
||||
onClick={this.handleSubmit}
|
||||
id='upload-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + this.props.intl.formatMessage(holders.uploading)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.license.upload'
|
||||
defaultMessage='Upload'
|
||||
/>
|
||||
</button>
|
||||
<div className='help-text no-margin'>
|
||||
{fileName}
|
||||
</div>
|
||||
<br/>
|
||||
{serverError}
|
||||
<p className='help-text no-margin'>
|
||||
<FormattedHTMLMessage
|
||||
id='admin.license.uploadDesc'
|
||||
defaultMessage='Upload a license key for Mattermost Enterprise Edition to upgrade this server. <a href="http://mattermost.com" target="_blank">Visit us online</a> to learn more about the benefits of Enterprise Edition or to purchase a key.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.license.title'
|
||||
defaultMessage='Edition and License'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.license.edition'
|
||||
defaultMessage='Edition: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
{edition}
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.license.type'
|
||||
defaultMessage='License: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
{licenseType}
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.license.key'
|
||||
defaultMessage='License Key: '
|
||||
/>
|
||||
</label>
|
||||
{licenseKey}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LicenseSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(LicenseSettings);
|
||||
418
webapp/components/admin_console/log_settings.jsx
Обычный файл
418
webapp/components/admin_console/log_settings.jsx
Обычный файл
@@ -0,0 +1,418 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
locationPlaceholder: {
|
||||
id: 'admin.log.locationPlaceholder',
|
||||
defaultMessage: 'Enter your file location'
|
||||
},
|
||||
formatPlaceholder: {
|
||||
id: 'admin.log.formatPlaceholder',
|
||||
defaultMessage: 'Enter your file format'
|
||||
},
|
||||
saving: {
|
||||
id: 'admin.log.saving',
|
||||
defaultMessage: 'Saving Config...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class LogSettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
consoleEnable: this.props.config.LogSettings.EnableConsole,
|
||||
fileEnable: this.props.config.LogSettings.EnableFile,
|
||||
saveNeeded: false,
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
handleChange(action) {
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
|
||||
if (action === 'console_true') {
|
||||
s.consoleEnable = true;
|
||||
}
|
||||
|
||||
if (action === 'console_false') {
|
||||
s.consoleEnable = false;
|
||||
}
|
||||
|
||||
if (action === 'file_true') {
|
||||
s.fileEnable = true;
|
||||
}
|
||||
|
||||
if (action === 'file_false') {
|
||||
s.fileEnable = false;
|
||||
}
|
||||
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('#save-button').button('loading');
|
||||
|
||||
var config = this.props.config;
|
||||
config.LogSettings.EnableConsole = ReactDOM.findDOMNode(this.refs.consoleEnable).checked;
|
||||
config.LogSettings.ConsoleLevel = ReactDOM.findDOMNode(this.refs.consoleLevel).value;
|
||||
config.LogSettings.EnableFile = ReactDOM.findDOMNode(this.refs.fileEnable).checked;
|
||||
config.LogSettings.FileLevel = ReactDOM.findDOMNode(this.refs.fileLevel).value;
|
||||
config.LogSettings.FileLocation = ReactDOM.findDOMNode(this.refs.fileLocation).value.trim();
|
||||
config.LogSettings.FileFormat = ReactDOM.findDOMNode(this.refs.fileFormat).value.trim();
|
||||
|
||||
Client.saveConfig(
|
||||
config,
|
||||
() => {
|
||||
AsyncClient.getConfig();
|
||||
this.setState({
|
||||
consoleEnable: config.LogSettings.EnableConsole,
|
||||
fileEnable: config.LogSettings.EnableFile,
|
||||
serverError: null,
|
||||
saveNeeded: false
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
consoleEnable: config.LogSettings.EnableConsole,
|
||||
fileEnable: config.LogSettings.EnableFile,
|
||||
serverError: err.message,
|
||||
saveNeeded: true
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var saveClass = 'btn';
|
||||
if (this.state.saveNeeded) {
|
||||
saveClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.log.logSettings'
|
||||
defaultMessage='Log Settings'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='consoleEnable'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.log.consoleTitle'
|
||||
defaultMessage='Log To The Console: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='consoleEnable'
|
||||
value='true'
|
||||
ref='consoleEnable'
|
||||
defaultChecked={this.props.config.LogSettings.EnableConsole}
|
||||
onChange={this.handleChange.bind(this, 'console_true')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.log.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='consoleEnable'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.LogSettings.EnableConsole}
|
||||
onChange={this.handleChange.bind(this, 'console_false')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.log.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.consoleDescription'
|
||||
defaultMessage='Typically set to false in production. Developers may set this field to true to output log messages to console based on the console level option. If true, server writes messages to the standard output stream (stdout).'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='consoleLevel'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.log.levelTitle'
|
||||
defaultMessage='Console Log Level:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<select
|
||||
className='form-control'
|
||||
id='consoleLevel'
|
||||
ref='consoleLevel'
|
||||
defaultValue={this.props.config.LogSettings.consoleLevel}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.consoleEnable}
|
||||
>
|
||||
<option value='DEBUG'>{'DEBUG'}</option>
|
||||
<option value='INFO'>{'INFO'}</option>
|
||||
<option value='ERROR'>{'ERROR'}</option>
|
||||
</select>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.levelDescription'
|
||||
defaultMessage='This setting determines the level of detail at which log events are written to the console. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.log.fileTitle'
|
||||
defaultMessage='Log To File: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='fileEnable'
|
||||
ref='fileEnable'
|
||||
value='true'
|
||||
defaultChecked={this.props.config.LogSettings.EnableFile}
|
||||
onChange={this.handleChange.bind(this, 'file_true')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.log.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='fileEnable'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.LogSettings.EnableFile}
|
||||
onChange={this.handleChange.bind(this, 'file_false')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.log.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.fileDescription'
|
||||
defaultMessage='Typically set to true in production. When true, log files are written to the log file specified in file location field below.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='fileLevel'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.log.fileLevelTitle'
|
||||
defaultMessage='File Log Level:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<select
|
||||
className='form-control'
|
||||
id='fileLevel'
|
||||
ref='fileLevel'
|
||||
defaultValue={this.props.config.LogSettings.FileLevel}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.fileEnable}
|
||||
>
|
||||
<option value='DEBUG'>{'DEBUG'}</option>
|
||||
<option value='INFO'>{'INFO'}</option>
|
||||
<option value='ERROR'>{'ERROR'}</option>
|
||||
</select>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.fileLevelDescription'
|
||||
defaultMessage='This setting determines the level of detail at which log events are written to the log file. ERROR: Outputs only error messages. INFO: Outputs error messages and information around startup and initialization. DEBUG: Prints high detail for developers working on debugging issues.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='fileLocation'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.log.locationTitle'
|
||||
defaultMessage='File Location:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='fileLocation'
|
||||
ref='fileLocation'
|
||||
placeholder={formatMessage(holders.locationPlaceholder)}
|
||||
defaultValue={this.props.config.LogSettings.FileLocation}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.fileEnable}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.locationDescription'
|
||||
defaultMessage='File to which log files are written. If blank, will be set to ./logs/mattermost, which writes logs to mattermost.log. Log rotation is enabled and every 10,000 lines of log information is written to new files stored in the same directory, for example mattermost.2015-09-23.001, mattermost.2015-09-23.002, and so forth.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='fileFormat'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.log.formatTitle'
|
||||
defaultMessage='File Format:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='fileFormat'
|
||||
ref='fileFormat'
|
||||
placeholder={formatMessage(holders.formatPlaceholder)}
|
||||
defaultValue={this.props.config.LogSettings.FileFormat}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.fileEnable}
|
||||
/>
|
||||
<div className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.formatDescription'
|
||||
defaultMessage='Format of log message output. If blank will be set to "[%D %T] [%L] %M", where:'
|
||||
/>
|
||||
<div className='help-text'>
|
||||
<table
|
||||
className='table table-bordered'
|
||||
cellPadding='5'
|
||||
>
|
||||
<tbody>
|
||||
<tr><td className='help-text'>{'%T'}</td><td className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.formatTime'
|
||||
defaultMessage='Time (15:04:05 MST)'
|
||||
/>
|
||||
</td></tr>
|
||||
<tr><td className='help-text'>{'%D'}</td><td className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.formatDateLong'
|
||||
defaultMessage='Date (2006/01/02)'
|
||||
/>
|
||||
</td></tr>
|
||||
<tr><td className='help-text'>{'%d'}</td><td className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.formatDateShort'
|
||||
defaultMessage='Date (01/02/06)'
|
||||
/>
|
||||
</td></tr>
|
||||
<tr><td className='help-text'>{'%L'}</td><td className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.formatLevel'
|
||||
defaultMessage='Level (DEBG, INFO, EROR)'
|
||||
/>
|
||||
</td></tr>
|
||||
<tr><td className='help-text'>{'%S'}</td><td className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.formatSource'
|
||||
defaultMessage='Source'
|
||||
/>
|
||||
</td></tr>
|
||||
<tr><td className='help-text'>{'%M'}</td><td className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.log.formatMessage'
|
||||
defaultMessage='Message'
|
||||
/>
|
||||
</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
{serverError}
|
||||
<button
|
||||
disabled={!this.state.saveNeeded}
|
||||
type='submit'
|
||||
className={saveClass}
|
||||
onClick={this.handleSubmit}
|
||||
id='save-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.log.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LogSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(LogSettings);
|
||||
102
webapp/components/admin_console/logs.jsx
Обычный файл
102
webapp/components/admin_console/logs.jsx
Обычный файл
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import AdminStore from 'stores/admin_store.jsx';
|
||||
import LoadingScreen from '../loading_screen.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class Logs extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onLogListenerChange = this.onLogListenerChange.bind(this);
|
||||
this.reload = this.reload.bind(this);
|
||||
|
||||
this.state = {
|
||||
logs: AdminStore.getLogs()
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AdminStore.addLogChangeListener(this.onLogListenerChange);
|
||||
AsyncClient.getLogs();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AdminStore.removeLogChangeListener(this.onLogListenerChange);
|
||||
}
|
||||
|
||||
onLogListenerChange() {
|
||||
this.setState({
|
||||
logs: AdminStore.getLogs()
|
||||
});
|
||||
}
|
||||
|
||||
reload() {
|
||||
AdminStore.saveLogs(null);
|
||||
this.setState({
|
||||
logs: null
|
||||
});
|
||||
|
||||
AsyncClient.getLogs();
|
||||
}
|
||||
|
||||
render() {
|
||||
var content = null;
|
||||
|
||||
if (this.state.logs === null) {
|
||||
content = <LoadingScreen/>;
|
||||
} else {
|
||||
content = [];
|
||||
|
||||
for (var i = 0; i < this.state.logs.length; i++) {
|
||||
var style = {
|
||||
whiteSpace: 'nowrap',
|
||||
fontFamily: 'monospace'
|
||||
};
|
||||
|
||||
if (this.state.logs[i].indexOf('[EROR]') > 0) {
|
||||
style.color = 'red';
|
||||
}
|
||||
|
||||
content.push(<br key={'br_' + i}/>);
|
||||
content.push(
|
||||
<span
|
||||
key={'log_' + i}
|
||||
style={style}
|
||||
>
|
||||
{this.state.logs[i]}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='panel'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.logs.title'
|
||||
defaultMessage='Server Logs'
|
||||
/>
|
||||
</h3>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
onClick={this.reload}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.logs.reload'
|
||||
defaultMessage='Reload'
|
||||
/>
|
||||
</button>
|
||||
<div className='log__panel'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
215
webapp/components/admin_console/privacy_settings.jsx
Обычный файл
215
webapp/components/admin_console/privacy_settings.jsx
Обычный файл
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
saving: {
|
||||
id: 'admin.privacy.saving',
|
||||
defaultMessage: 'Saving Config...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class PrivacySettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
saveNeeded: false,
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('#save-button').button('loading');
|
||||
|
||||
var config = this.props.config;
|
||||
config.PrivacySettings.ShowEmailAddress = ReactDOM.findDOMNode(this.refs.ShowEmailAddress).checked;
|
||||
config.PrivacySettings.ShowFullName = ReactDOM.findDOMNode(this.refs.ShowFullName).checked;
|
||||
|
||||
Client.saveConfig(
|
||||
config,
|
||||
() => {
|
||||
AsyncClient.getConfig();
|
||||
this.setState({
|
||||
serverError: null,
|
||||
saveNeeded: false
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
serverError: err.message,
|
||||
saveNeeded: true
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var saveClass = 'btn';
|
||||
if (this.state.saveNeeded) {
|
||||
saveClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.privacy.title'
|
||||
defaultMessage='Privacy Settings'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='ShowEmailAddress'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.privacy.showEmailTitle'
|
||||
defaultMessage='Show Email Address: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='ShowEmailAddress'
|
||||
value='true'
|
||||
ref='ShowEmailAddress'
|
||||
defaultChecked={this.props.config.PrivacySettings.ShowEmailAddress}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.privacy.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='ShowEmailAddress'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.PrivacySettings.ShowEmailAddress}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.privacy.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.privacy.showEmailDescription'
|
||||
defaultMessage='When false, hides email address of users from other users in the user interface, including team owners and team administrators. Used when system is set up for managing teams where some users choose to keep their contact information private.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='ShowFullName'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.privacy.showFullNameTitle'
|
||||
defaultMessage='Show Full Name: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='ShowFullName'
|
||||
value='true'
|
||||
ref='ShowFullName'
|
||||
defaultChecked={this.props.config.PrivacySettings.ShowFullName}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.privacy.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='ShowFullName'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.PrivacySettings.ShowFullName}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.privacy.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.privacy.showFullNameDescription'
|
||||
defaultMessage='When false, hides full name of users from other users, including team owners and team administrators. Username is shown in place of full name.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
{serverError}
|
||||
<button
|
||||
disabled={!this.state.saveNeeded}
|
||||
type='submit'
|
||||
className={saveClass}
|
||||
onClick={this.handleSubmit}
|
||||
id='save-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + this.props.intl.formatMessage(holders.saving)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.privacy.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PrivacySettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(PrivacySettings);
|
||||
371
webapp/components/admin_console/rate_settings.jsx
Обычный файл
371
webapp/components/admin_console/rate_settings.jsx
Обычный файл
@@ -0,0 +1,371 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
queriesExample: {
|
||||
id: 'admin.rate.queriesExample',
|
||||
defaultMessage: 'Ex "10"'
|
||||
},
|
||||
memoryExample: {
|
||||
id: 'admin.rate.memoryExample',
|
||||
defaultMessage: 'Ex "10000"'
|
||||
},
|
||||
httpHeaderExample: {
|
||||
id: 'admin.rate.httpHeaderExample',
|
||||
defaultMessage: 'Ex "X-Real-IP", "X-Forwarded-For"'
|
||||
},
|
||||
saving: {
|
||||
id: 'admin.rate.saving',
|
||||
defaultMessage: 'Saving Config...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class RateSettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
EnableRateLimiter: this.props.config.RateLimitSettings.EnableRateLimiter,
|
||||
VaryByRemoteAddr: this.props.config.RateLimitSettings.VaryByRemoteAddr,
|
||||
saveNeeded: false,
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
handleChange(action) {
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
|
||||
if (action === 'EnableRateLimiterTrue') {
|
||||
s.EnableRateLimiter = true;
|
||||
}
|
||||
|
||||
if (action === 'EnableRateLimiterFalse') {
|
||||
s.EnableRateLimiter = false;
|
||||
}
|
||||
|
||||
if (action === 'VaryByRemoteAddrTrue') {
|
||||
s.VaryByRemoteAddr = true;
|
||||
}
|
||||
|
||||
if (action === 'VaryByRemoteAddrFalse') {
|
||||
s.VaryByRemoteAddr = false;
|
||||
}
|
||||
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('#save-button').button('loading');
|
||||
|
||||
var config = this.props.config;
|
||||
config.RateLimitSettings.EnableRateLimiter = ReactDOM.findDOMNode(this.refs.EnableRateLimiter).checked;
|
||||
config.RateLimitSettings.VaryByRemoteAddr = ReactDOM.findDOMNode(this.refs.VaryByRemoteAddr).checked;
|
||||
config.RateLimitSettings.VaryByHeader = ReactDOM.findDOMNode(this.refs.VaryByHeader).value.trim();
|
||||
|
||||
var PerSec = 10;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.PerSec).value, 10))) {
|
||||
PerSec = parseInt(ReactDOM.findDOMNode(this.refs.PerSec).value, 10);
|
||||
}
|
||||
config.RateLimitSettings.PerSec = PerSec;
|
||||
ReactDOM.findDOMNode(this.refs.PerSec).value = PerSec;
|
||||
|
||||
var MemoryStoreSize = 10000;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value, 10))) {
|
||||
MemoryStoreSize = parseInt(ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value, 10);
|
||||
}
|
||||
config.RateLimitSettings.MemoryStoreSize = MemoryStoreSize;
|
||||
ReactDOM.findDOMNode(this.refs.MemoryStoreSize).value = MemoryStoreSize;
|
||||
|
||||
Client.saveConfig(
|
||||
config,
|
||||
() => {
|
||||
AsyncClient.getConfig();
|
||||
this.setState({
|
||||
serverError: null,
|
||||
saveNeeded: false
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
serverError: err.message,
|
||||
saveNeeded: true
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var saveClass = 'btn';
|
||||
if (this.state.saveNeeded) {
|
||||
saveClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
|
||||
<div className='banner'>
|
||||
<div className='banner__content'>
|
||||
<h4 className='banner__heading'>
|
||||
<FormattedMessage
|
||||
id='admin.rate.noteTitle'
|
||||
defaultMessage='Note:'
|
||||
/>
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='admin.rate.noteDescription'
|
||||
defaultMessage='Changing properties in this section will require a server restart before taking effect.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.rate.title'
|
||||
defaultMessage='Rate Limit Settings'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableRateLimiter'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.rate.enableLimiterTitle'
|
||||
defaultMessage='Enable Rate Limiter: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableRateLimiter'
|
||||
value='true'
|
||||
ref='EnableRateLimiter'
|
||||
defaultChecked={this.props.config.RateLimitSettings.EnableRateLimiter}
|
||||
onChange={this.handleChange.bind(this, 'EnableRateLimiterTrue')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.rate.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableRateLimiter'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.RateLimitSettings.EnableRateLimiter}
|
||||
onChange={this.handleChange.bind(this, 'EnableRateLimiterFalse')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.rate.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.rate.enableLimiterDescription'
|
||||
defaultMessage='When true, APIs are throttled at rates specified below.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='PerSec'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.rate.queriesTitle'
|
||||
defaultMessage='Number Of Queries Per Second:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='PerSec'
|
||||
ref='PerSec'
|
||||
placeholder={formatMessage(holders.queriesExample)}
|
||||
defaultValue={this.props.config.RateLimitSettings.PerSec}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.EnableRateLimiter}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.rate.queriesDescription'
|
||||
defaultMessage='Throttles API at this number of requests per second.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='MemoryStoreSize'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.rate.memoryTitle'
|
||||
defaultMessage='Memory Store Size:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='MemoryStoreSize'
|
||||
ref='MemoryStoreSize'
|
||||
placeholder={formatMessage(holders.memoryExample)}
|
||||
defaultValue={this.props.config.RateLimitSettings.MemoryStoreSize}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.EnableRateLimiter}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.rate.memoryDescription'
|
||||
defaultMessage='Maximum number of users sessions connected to the system as determined by "Vary By Remote Address" and "Vary By Header" settings below.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='VaryByRemoteAddr'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.rate.remoteTitle'
|
||||
defaultMessage='Vary By Remote Address: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='VaryByRemoteAddr'
|
||||
value='true'
|
||||
ref='VaryByRemoteAddr'
|
||||
defaultChecked={this.props.config.RateLimitSettings.VaryByRemoteAddr}
|
||||
onChange={this.handleChange.bind(this, 'VaryByRemoteAddrTrue')}
|
||||
disabled={!this.state.EnableRateLimiter}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.rate.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='VaryByRemoteAddr'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.RateLimitSettings.VaryByRemoteAddr}
|
||||
onChange={this.handleChange.bind(this, 'VaryByRemoteAddrFalse')}
|
||||
disabled={!this.state.EnableRateLimiter}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.rate.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.rate.remoteDescription'
|
||||
defaultMessage='When true, rate limit API access by IP address.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='VaryByHeader'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.rate.httpHeaderTitle'
|
||||
defaultMessage='Vary By HTTP Header:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='VaryByHeader'
|
||||
ref='VaryByHeader'
|
||||
placeholder={formatMessage(holders.httpHeaderExample)}
|
||||
defaultValue={this.props.config.RateLimitSettings.VaryByHeader}
|
||||
onChange={this.handleChange}
|
||||
disabled={!this.state.EnableRateLimiter || this.state.VaryByRemoteAddr}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.rate.httpHeaderDescription'
|
||||
defaultMessage='When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring NGINX set to "X-Real-IP", when configuring AmazonELB set to "X-Forwarded-For").'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
{serverError}
|
||||
<button
|
||||
disabled={!this.state.saveNeeded}
|
||||
type='submit'
|
||||
className={saveClass}
|
||||
onClick={this.handleSubmit}
|
||||
id='save-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.rate.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RateSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(RateSettings);
|
||||
162
webapp/components/admin_console/reset_password_modal.jsx
Обычный файл
162
webapp/components/admin_console/reset_password_modal.jsx
Обычный файл
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
var holders = defineMessages({
|
||||
submit: {
|
||||
id: 'admin.reset_password.submit',
|
||||
defaultMessage: 'Please enter at least {chars} characters.'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class ResetPasswordModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.doSubmit = this.doSubmit.bind(this);
|
||||
this.doCancel = this.doCancel.bind(this);
|
||||
|
||||
this.state = {
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
doSubmit(e) {
|
||||
e.preventDefault();
|
||||
var password = ReactDOM.findDOMNode(this.refs.password).value;
|
||||
|
||||
if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) {
|
||||
this.setState({serverError: this.props.intl.formatMessage(holders.submit, {chars: Constants.MIN_PASSWORD_LENGTH})});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({serverError: null});
|
||||
|
||||
var data = {};
|
||||
data.new_password = password;
|
||||
data.name = this.props.team.name;
|
||||
data.user_id = this.props.user.id;
|
||||
|
||||
Client.resetPassword(data,
|
||||
() => {
|
||||
this.props.onModalSubmit(ReactDOM.findDOMNode(this.refs.password).value);
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
doCancel() {
|
||||
this.setState({serverError: null});
|
||||
this.props.onModalDismissed();
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.user == null) {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
let urlClass = 'input-group input-group--limit';
|
||||
let serverError = null;
|
||||
|
||||
if (this.state.serverError) {
|
||||
urlClass += ' has-error';
|
||||
serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.doCancel}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='admin.reset_password.title'
|
||||
defaultMessage='Reset Password'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<form
|
||||
role='form'
|
||||
className='form-horizontal'
|
||||
>
|
||||
<Modal.Body>
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-10'>
|
||||
<div className={urlClass}>
|
||||
<span
|
||||
data-toggle='tooltip'
|
||||
title='New Password'
|
||||
className='input-group-addon'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.reset_password.newPassword'
|
||||
defaultMessage='New Password'
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
type='password'
|
||||
ref='password'
|
||||
className='form-control'
|
||||
maxLength='22'
|
||||
autoFocus={true}
|
||||
tabIndex='1'
|
||||
/>
|
||||
</div>
|
||||
{serverError}
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.doCancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.reset_password.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={this.doSubmit}
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
tabIndex='2'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.reset_password.select'
|
||||
defaultMessage='Select'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ResetPasswordModal.defaultProps = {
|
||||
show: false
|
||||
};
|
||||
|
||||
ResetPasswordModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
user: React.PropTypes.object,
|
||||
team: React.PropTypes.object,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onModalSubmit: React.PropTypes.func,
|
||||
onModalDismissed: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default injectIntl(ResetPasswordModal);
|
||||
115
webapp/components/admin_console/select_team_modal.jsx
Обычный файл
115
webapp/components/admin_console/select_team_modal.jsx
Обычный файл
@@ -0,0 +1,115 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class SelectTeamModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.doSubmit = this.doSubmit.bind(this);
|
||||
this.doCancel = this.doCancel.bind(this);
|
||||
}
|
||||
|
||||
doSubmit(e) {
|
||||
e.preventDefault();
|
||||
this.props.onModalSubmit(ReactDOM.findDOMNode(this.refs.team).value);
|
||||
}
|
||||
doCancel() {
|
||||
this.props.onModalDismissed();
|
||||
}
|
||||
render() {
|
||||
if (this.props.teams == null) {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
var options = [];
|
||||
|
||||
for (var key in this.props.teams) {
|
||||
if (this.props.teams.hasOwnProperty(key)) {
|
||||
var team = this.props.teams[key];
|
||||
options.push(
|
||||
<option
|
||||
key={'opt_' + team.id}
|
||||
value={team.id}
|
||||
>
|
||||
{team.name}
|
||||
</option>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.doCancel}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='admin.select_team.selectTeam'
|
||||
defaultMessage='Select Team'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<form
|
||||
role='form'
|
||||
className='form-horizontal'
|
||||
>
|
||||
<Modal.Body>
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
<select
|
||||
ref='team'
|
||||
size='10'
|
||||
className='form-control'
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.doCancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.select_team.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={this.doSubmit}
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
tabIndex='2'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.select_team.select'
|
||||
defaultMessage='Select'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectTeamModal.defaultProps = {
|
||||
show: false
|
||||
};
|
||||
|
||||
SelectTeamModal.propTypes = {
|
||||
teams: React.PropTypes.object,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onModalSubmit: React.PropTypes.func,
|
||||
onModalDismissed: React.PropTypes.func
|
||||
};
|
||||
984
webapp/components/admin_console/service_settings.jsx
Обычный файл
984
webapp/components/admin_console/service_settings.jsx
Обычный файл
@@ -0,0 +1,984 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||
|
||||
const DefaultSessionLength = 30;
|
||||
const DefaultMaximumLoginAttempts = 10;
|
||||
const DefaultSessionCacheInMinutes = 10;
|
||||
|
||||
var holders = defineMessages({
|
||||
listenExample: {
|
||||
id: 'admin.service.listenExample',
|
||||
defaultMessage: 'Ex ":8065"'
|
||||
},
|
||||
attemptExample: {
|
||||
id: 'admin.service.attemptExample',
|
||||
defaultMessage: 'Ex "10"'
|
||||
},
|
||||
segmentExample: {
|
||||
id: 'admin.service.segmentExample',
|
||||
defaultMessage: 'Ex "g3fgGOXJAQ43QV7rAh6iwQCkV4cA1Gs"'
|
||||
},
|
||||
googleExample: {
|
||||
id: 'admin.service.googleExample',
|
||||
defaultMessage: 'Ex "7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV"'
|
||||
},
|
||||
sessionDaysEx: {
|
||||
id: 'admin.service.sessionDaysEx',
|
||||
defaultMessage: 'Ex "30"'
|
||||
},
|
||||
corsExample: {
|
||||
id: 'admin.service.corsEx',
|
||||
defaultMessage: 'http://example.com'
|
||||
},
|
||||
saving: {
|
||||
id: 'admin.service.saving',
|
||||
defaultMessage: 'Saving Config...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class ServiceSettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
saveNeeded: false,
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('#save-button').button('loading');
|
||||
|
||||
var config = this.props.config;
|
||||
config.ServiceSettings.ListenAddress = ReactDOM.findDOMNode(this.refs.ListenAddress).value.trim();
|
||||
if (config.ServiceSettings.ListenAddress === '') {
|
||||
config.ServiceSettings.ListenAddress = ':8065';
|
||||
ReactDOM.findDOMNode(this.refs.ListenAddress).value = config.ServiceSettings.ListenAddress;
|
||||
}
|
||||
|
||||
config.ServiceSettings.SegmentDeveloperKey = ReactDOM.findDOMNode(this.refs.SegmentDeveloperKey).value.trim();
|
||||
config.ServiceSettings.GoogleDeveloperKey = ReactDOM.findDOMNode(this.refs.GoogleDeveloperKey).value.trim();
|
||||
config.ServiceSettings.EnableIncomingWebhooks = ReactDOM.findDOMNode(this.refs.EnableIncomingWebhooks).checked;
|
||||
config.ServiceSettings.EnableOutgoingWebhooks = ReactDOM.findDOMNode(this.refs.EnableOutgoingWebhooks).checked;
|
||||
config.ServiceSettings.EnablePostUsernameOverride = ReactDOM.findDOMNode(this.refs.EnablePostUsernameOverride).checked;
|
||||
config.ServiceSettings.EnablePostIconOverride = ReactDOM.findDOMNode(this.refs.EnablePostIconOverride).checked;
|
||||
config.ServiceSettings.EnableTesting = ReactDOM.findDOMNode(this.refs.EnableTesting).checked;
|
||||
config.ServiceSettings.EnableDeveloper = ReactDOM.findDOMNode(this.refs.EnableDeveloper).checked;
|
||||
config.ServiceSettings.EnableSecurityFixAlert = ReactDOM.findDOMNode(this.refs.EnableSecurityFixAlert).checked;
|
||||
config.ServiceSettings.EnableInsecureOutgoingConnections = ReactDOM.findDOMNode(this.refs.EnableInsecureOutgoingConnections).checked;
|
||||
config.ServiceSettings.EnableCommands = ReactDOM.findDOMNode(this.refs.EnableCommands).checked;
|
||||
config.ServiceSettings.EnableOnlyAdminIntegrations = ReactDOM.findDOMNode(this.refs.EnableOnlyAdminIntegrations).checked;
|
||||
|
||||
//config.ServiceSettings.EnableOAuthServiceProvider = ReactDOM.findDOMNode(this.refs.EnableOAuthServiceProvider).checked;
|
||||
|
||||
var MaximumLoginAttempts = DefaultMaximumLoginAttempts;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value, 10))) {
|
||||
MaximumLoginAttempts = parseInt(ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value, 10);
|
||||
}
|
||||
if (MaximumLoginAttempts < 1) {
|
||||
MaximumLoginAttempts = 1;
|
||||
}
|
||||
config.ServiceSettings.MaximumLoginAttempts = MaximumLoginAttempts;
|
||||
ReactDOM.findDOMNode(this.refs.MaximumLoginAttempts).value = MaximumLoginAttempts;
|
||||
|
||||
var SessionLengthWebInDays = DefaultSessionLength;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value, 10))) {
|
||||
SessionLengthWebInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value, 10);
|
||||
}
|
||||
if (SessionLengthWebInDays < 1) {
|
||||
SessionLengthWebInDays = 1;
|
||||
}
|
||||
config.ServiceSettings.SessionLengthWebInDays = SessionLengthWebInDays;
|
||||
ReactDOM.findDOMNode(this.refs.SessionLengthWebInDays).value = SessionLengthWebInDays;
|
||||
|
||||
var SessionLengthMobileInDays = DefaultSessionLength;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value, 10))) {
|
||||
SessionLengthMobileInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value, 10);
|
||||
}
|
||||
if (SessionLengthMobileInDays < 1) {
|
||||
SessionLengthMobileInDays = 1;
|
||||
}
|
||||
config.ServiceSettings.SessionLengthMobileInDays = SessionLengthMobileInDays;
|
||||
ReactDOM.findDOMNode(this.refs.SessionLengthMobileInDays).value = SessionLengthMobileInDays;
|
||||
|
||||
var SessionLengthSSOInDays = DefaultSessionLength;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value, 10))) {
|
||||
SessionLengthSSOInDays = parseInt(ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value, 10);
|
||||
}
|
||||
if (SessionLengthSSOInDays < 1) {
|
||||
SessionLengthSSOInDays = 1;
|
||||
}
|
||||
config.ServiceSettings.SessionLengthSSOInDays = SessionLengthSSOInDays;
|
||||
ReactDOM.findDOMNode(this.refs.SessionLengthSSOInDays).value = SessionLengthSSOInDays;
|
||||
|
||||
var SessionCacheInMinutes = DefaultSessionCacheInMinutes;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value, 10))) {
|
||||
SessionCacheInMinutes = parseInt(ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value, 10);
|
||||
}
|
||||
if (SessionCacheInMinutes < -1) {
|
||||
SessionCacheInMinutes = -1;
|
||||
}
|
||||
config.ServiceSettings.SessionCacheInMinutes = SessionCacheInMinutes;
|
||||
ReactDOM.findDOMNode(this.refs.SessionCacheInMinutes).value = SessionCacheInMinutes;
|
||||
|
||||
config.ServiceSettings.AllowCorsFrom = ReactDOM.findDOMNode(this.refs.AllowCorsFrom).value.trim();
|
||||
|
||||
Client.saveConfig(
|
||||
config,
|
||||
() => {
|
||||
AsyncClient.getConfig();
|
||||
this.setState({
|
||||
serverError: null,
|
||||
saveNeeded: false
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
serverError: err.message,
|
||||
saveNeeded: true
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var saveClass = 'btn';
|
||||
if (this.state.saveNeeded) {
|
||||
saveClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.service.title'
|
||||
defaultMessage='Service Settings'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='ListenAddress'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.listenAddress'
|
||||
defaultMessage='Listen Address:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='ListenAddress'
|
||||
ref='ListenAddress'
|
||||
placeholder={formatMessage(holders.listenExample)}
|
||||
defaultValue={this.props.config.ServiceSettings.ListenAddress}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.listenDescription'
|
||||
defaultMessage='The address to which to bind and listen. Entering ":8065" will bind to all interfaces or you can choose one like "127.0.0.1:8065". Changing this will require a server restart before taking effect.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='MaximumLoginAttempts'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.attemptTitle'
|
||||
defaultMessage='Maximum Login Attempts:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='MaximumLoginAttempts'
|
||||
ref='MaximumLoginAttempts'
|
||||
placeholder={formatMessage(holders.attemptExample)}
|
||||
defaultValue={this.props.config.ServiceSettings.MaximumLoginAttempts}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.attemptDescription'
|
||||
defaultMessage='Login attempts allowed before user is locked out and required to reset password via email.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='SegmentDeveloperKey'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.segmentTitle'
|
||||
defaultMessage='Segment Developer Key:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='SegmentDeveloperKey'
|
||||
ref='SegmentDeveloperKey'
|
||||
placeholder={formatMessage(holders.segmentExample)}
|
||||
defaultValue={this.props.config.ServiceSettings.SegmentDeveloperKey}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.segmentDescription'
|
||||
defaultMessage='For users running a SaaS services, sign up for a key at Segment.com to track metrics.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='GoogleDeveloperKey'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.googleTitle'
|
||||
defaultMessage='Google Developer Key:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='GoogleDeveloperKey'
|
||||
ref='GoogleDeveloperKey'
|
||||
placeholder={formatMessage(holders.googleExample)}
|
||||
defaultValue={this.props.config.ServiceSettings.GoogleDeveloperKey}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedHTMLMessage
|
||||
id='admin.service.googleDescription'
|
||||
defaultMessage='Set this key to enable embedding of YouTube video previews based on hyperlinks appearing in messages or comments. Instructions to obtain a key available at <a href="https://www.youtube.com/watch?v=Im69kzhpR3I" target="_blank">https://www.youtube.com/watch?v=Im69kzhpR3I</a>. Leaving the field blank disables the automatic generation of YouTube video previews from links.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableIncomingWebhooks'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.webhooksTitle'
|
||||
defaultMessage='Enable Incoming Webhooks: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableIncomingWebhooks'
|
||||
value='true'
|
||||
ref='EnableIncomingWebhooks'
|
||||
defaultChecked={this.props.config.ServiceSettings.EnableIncomingWebhooks}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableIncomingWebhooks'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.ServiceSettings.EnableIncomingWebhooks}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.webhooksDescription'
|
||||
defaultMessage='When true, incoming webhooks will be allowed. To help combat phishing attacks, all posts from webhooks will be labelled by a BOT tag.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableOutgoingWebhooks'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.outWebhooksTitle'
|
||||
defaultMessage='Enable Outgoing Webhooks: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableOutgoingWebhooks'
|
||||
value='true'
|
||||
ref='EnableOutgoingWebhooks'
|
||||
defaultChecked={this.props.config.ServiceSettings.EnableOutgoingWebhooks}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableOutgoingWebhooks'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.ServiceSettings.EnableOutgoingWebhooks}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.outWebhooksDesc'
|
||||
defaultMessage='When true, outgoing webhooks will be allowed.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableCommands'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.cmdsTitle'
|
||||
defaultMessage='Enable Slash Commands: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableCommands'
|
||||
value='true'
|
||||
ref='EnableCommands'
|
||||
defaultChecked={this.props.config.ServiceSettings.EnableCommands}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableCommands'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.ServiceSettings.EnableCommands}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.cmdsDesc'
|
||||
defaultMessage='When true, user created slash commands will be allowed.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableOnlyAdminIntegrations'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.integrationAdmin'
|
||||
defaultMessage='Enable Integrations for Admin Only: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableOnlyAdminIntegrations'
|
||||
value='true'
|
||||
ref='EnableOnlyAdminIntegrations'
|
||||
defaultChecked={this.props.config.ServiceSettings.EnableOnlyAdminIntegrations}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableOnlyAdminIntegrations'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.ServiceSettings.EnableOnlyAdminIntegrations}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.integrationAdminDesc'
|
||||
defaultMessage='When true, user created integrations can only be created by admins.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnablePostUsernameOverride'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.overrideTitle'
|
||||
defaultMessage='Enable Overriding Usernames from Webhooks and Slash Commands: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnablePostUsernameOverride'
|
||||
value='true'
|
||||
ref='EnablePostUsernameOverride'
|
||||
defaultChecked={this.props.config.ServiceSettings.EnablePostUsernameOverride}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnablePostUsernameOverride'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.ServiceSettings.EnablePostUsernameOverride}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.overrideDescription'
|
||||
defaultMessage='When true, webhooks and slash commands will be allowed to change the username they are posting as. Note, combined with allowing icon overriding, this could open users up to phishing attacks.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnablePostIconOverride'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.iconTitle'
|
||||
defaultMessage='Enable Overriding Icon from Webhooks and Slash Commands: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnablePostIconOverride'
|
||||
value='true'
|
||||
ref='EnablePostIconOverride'
|
||||
defaultChecked={this.props.config.ServiceSettings.EnablePostIconOverride}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnablePostIconOverride'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.ServiceSettings.EnablePostIconOverride}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.iconDescription'
|
||||
defaultMessage='When true, webhooks and slash commands will be allowed to change the icon they post with. Note, combined with allowing username overriding, this could open users up to phishing attacks.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableTesting'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.testingTitle'
|
||||
defaultMessage='Enable Testing: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableTesting'
|
||||
value='true'
|
||||
ref='EnableTesting'
|
||||
defaultChecked={this.props.config.ServiceSettings.EnableTesting}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableTesting'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.ServiceSettings.EnableTesting}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.testingDescription'
|
||||
defaultMessage='(Developer Option) When true, /loadtest slash command is enabled to load test accounts and test data. Changing this will require a server restart before taking effect.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableDeveloper'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.developerTitle'
|
||||
defaultMessage='Enable Developer Mode: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableDeveloper'
|
||||
value='true'
|
||||
ref='EnableDeveloper'
|
||||
defaultChecked={this.props.config.ServiceSettings.EnableDeveloper}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableDeveloper'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.ServiceSettings.EnableDeveloper}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.developerDesc'
|
||||
defaultMessage='(Developer Option) When true, extra information around errors will be displayed in the UI.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableSecurityFixAlert'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.securityTitle'
|
||||
defaultMessage='Enable Security Alerts: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableSecurityFixAlert'
|
||||
value='true'
|
||||
ref='EnableSecurityFixAlert'
|
||||
defaultChecked={this.props.config.ServiceSettings.EnableSecurityFixAlert}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableSecurityFixAlert'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.ServiceSettings.EnableSecurityFixAlert}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.securityDesc'
|
||||
defaultMessage='When true, System Administrators are notified by email if a relevant security fix alert has been announced in the last 12 hours. Requires email to be enabled.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableInsecureOutgoingConnections'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.insecureTlsTitle'
|
||||
defaultMessage='Enable Insecure Outgoing Connections: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableInsecureOutgoingConnections'
|
||||
value='true'
|
||||
ref='EnableInsecureOutgoingConnections'
|
||||
defaultChecked={this.props.config.ServiceSettings.EnableInsecureOutgoingConnections}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableInsecureOutgoingConnections'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.ServiceSettings.EnableInsecureOutgoingConnections}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.service.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.insecureTlsDesc'
|
||||
defaultMessage='When true, any outgoing HTTPS requests will accept unverified, self-signed certificates. For example, outgoing webhooks to a server with a self-signed TLS certificate, using any domain, will be allowed. Note that this makes these connections susceptible to man-in-the-middle attacks.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='AllowCorsFrom'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.corsTitle'
|
||||
defaultMessage='Allow Cross-origin Requests from:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='AllowCorsFrom'
|
||||
ref='AllowCorsFrom'
|
||||
placeholder={formatMessage(holders.corsExample)}
|
||||
defaultValue={this.props.config.ServiceSettings.AllowCorsFrom}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.corsDescription'
|
||||
defaultMessage='Enable HTTP Cross origin request from a specific domain. Use "*" if you want to allow CORS from any domain or leave it blank to disable it.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='SessionLengthWebInDays'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.webSessionDays'
|
||||
defaultMessage='Session Length for Web in Days:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='SessionLengthWebInDays'
|
||||
ref='SessionLengthWebInDays'
|
||||
placeholder={formatMessage(holders.sessionDaysEx)}
|
||||
defaultValue={this.props.config.ServiceSettings.SessionLengthWebInDays}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.webSessionDaysDesc'
|
||||
defaultMessage='The web session will expire after the number of days specified and will require a user to login again.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='SessionLengthMobileInDays'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.mobileSessionDays'
|
||||
defaultMessage='Session Length for Mobile Device in Days:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='SessionLengthMobileInDays'
|
||||
ref='SessionLengthMobileInDays'
|
||||
placeholder={formatMessage(holders.sessionDaysEx)}
|
||||
defaultValue={this.props.config.ServiceSettings.SessionLengthMobileInDays}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.mobileSessionDaysDesc'
|
||||
defaultMessage='The native mobile session will expire after the number of days specified and will require a user to login again.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='SessionLengthSSOInDays'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.ssoSessionDays'
|
||||
defaultMessage='Session Length for SSO in Days:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='SessionLengthSSOInDays'
|
||||
ref='SessionLengthSSOInDays'
|
||||
placeholder={formatMessage(holders.sessionDaysEx)}
|
||||
defaultValue={this.props.config.ServiceSettings.SessionLengthSSOInDays}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.ssoSessionDaysDesc'
|
||||
defaultMessage='The SSO session will expire after the number of days specified and will require a user to login again.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='SessionCacheInMinutes'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.sessionCache'
|
||||
defaultMessage='Session Cache in Minutes:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='SessionCacheInMinutes'
|
||||
ref='SessionCacheInMinutes'
|
||||
placeholder={formatMessage(holders.sessionDaysEx)}
|
||||
defaultValue={this.props.config.ServiceSettings.SessionCacheInMinutes}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.service.sessionCacheDesc'
|
||||
defaultMessage='The number of minutes to cache a session in memory.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
{serverError}
|
||||
<button
|
||||
disabled={!this.state.saveNeeded}
|
||||
type='submit'
|
||||
className={saveClass}
|
||||
onClick={this.handleSubmit}
|
||||
id='save-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.service.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// <div className='form-group'>
|
||||
// <label
|
||||
// className='control-label col-sm-4'
|
||||
// htmlFor='EnableOAuthServiceProvider'
|
||||
// >
|
||||
// {'Enable OAuth Service Provider: '}
|
||||
// </label>
|
||||
// <div className='col-sm-8'>
|
||||
// <label className='radio-inline'>
|
||||
// <input
|
||||
// type='radio'
|
||||
// name='EnableOAuthServiceProvider'
|
||||
// value='true'
|
||||
// ref='EnableOAuthServiceProvider'
|
||||
// defaultChecked={this.props.config.ServiceSettings.EnableOAuthServiceProvider}
|
||||
// onChange={this.handleChange}
|
||||
// />
|
||||
// {'true'}
|
||||
// </label>
|
||||
// <label className='radio-inline'>
|
||||
// <input
|
||||
// type='radio'
|
||||
// name='EnableOAuthServiceProvider'
|
||||
// value='false'
|
||||
// defaultChecked={!this.props.config.ServiceSettings.EnableOAuthServiceProvider}
|
||||
// onChange={this.handleChange}
|
||||
// />
|
||||
// {'false'}
|
||||
// </label>
|
||||
// <p className='help-text'>{'When enabled Mattermost will act as an OAuth2 Provider. Changing this will require a server restart before taking effect.'}</p>
|
||||
// </div>
|
||||
// </div>
|
||||
|
||||
ServiceSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(ServiceSettings);
|
||||
390
webapp/components/admin_console/sql_settings.jsx
Обычный файл
390
webapp/components/admin_console/sql_settings.jsx
Обычный файл
@@ -0,0 +1,390 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import crypto from 'crypto';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
warning: {
|
||||
id: 'admin.sql.warning',
|
||||
defaultMessage: 'Warning: re-generating this salt may cause some columns in the database to return empty results.'
|
||||
},
|
||||
maxConnectionsExample: {
|
||||
id: 'admin.sql.maxConnectionsExample',
|
||||
defaultMessage: 'Ex "10"'
|
||||
},
|
||||
maxOpenExample: {
|
||||
id: 'admin.sql.maxOpenExample',
|
||||
defaultMessage: 'Ex "10"'
|
||||
},
|
||||
keyExample: {
|
||||
id: 'admin.sql.keyExample',
|
||||
defaultMessage: 'Ex "gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6"'
|
||||
},
|
||||
saving: {
|
||||
id: 'admin.sql.saving',
|
||||
defaultMessage: 'Saving Config...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class SqlSettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleGenerate = this.handleGenerate.bind(this);
|
||||
|
||||
this.state = {
|
||||
saveNeeded: false,
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('#save-button').button('loading');
|
||||
|
||||
var config = this.props.config;
|
||||
config.SqlSettings.Trace = ReactDOM.findDOMNode(this.refs.Trace).checked;
|
||||
config.SqlSettings.AtRestEncryptKey = ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value.trim();
|
||||
|
||||
if (config.SqlSettings.AtRestEncryptKey === '') {
|
||||
config.SqlSettings.AtRestEncryptKey = crypto.randomBytes(256).toString('base64').substring(0, 32);
|
||||
ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = config.SqlSettings.AtRestEncryptKey;
|
||||
}
|
||||
|
||||
var MaxOpenConns = 10;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10))) {
|
||||
MaxOpenConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxOpenConns).value, 10);
|
||||
}
|
||||
config.SqlSettings.MaxOpenConns = MaxOpenConns;
|
||||
ReactDOM.findDOMNode(this.refs.MaxOpenConns).value = MaxOpenConns;
|
||||
|
||||
var MaxIdleConns = 10;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10))) {
|
||||
MaxIdleConns = parseInt(ReactDOM.findDOMNode(this.refs.MaxIdleConns).value, 10);
|
||||
}
|
||||
config.SqlSettings.MaxIdleConns = MaxIdleConns;
|
||||
ReactDOM.findDOMNode(this.refs.MaxIdleConns).value = MaxIdleConns;
|
||||
|
||||
Client.saveConfig(
|
||||
config,
|
||||
() => {
|
||||
AsyncClient.getConfig();
|
||||
this.setState({
|
||||
serverError: null,
|
||||
saveNeeded: false
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
serverError: err.message,
|
||||
saveNeeded: true
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleGenerate(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var cfm = global.window.confirm(this.props.intl.formatMessage(holders.warning));
|
||||
if (cfm === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReactDOM.findDOMNode(this.refs.AtRestEncryptKey).value = crypto.randomBytes(256).toString('base64').substring(0, 32);
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var saveClass = 'btn';
|
||||
if (this.state.saveNeeded) {
|
||||
saveClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
var dataSource = '**********' + this.props.config.SqlSettings.DataSource.substring(this.props.config.SqlSettings.DataSource.indexOf('@'));
|
||||
|
||||
var dataSourceReplicas = '';
|
||||
this.props.config.SqlSettings.DataSourceReplicas.forEach((replica) => {
|
||||
dataSourceReplicas += '[**********' + replica.substring(replica.indexOf('@')) + '] ';
|
||||
});
|
||||
|
||||
if (this.props.config.SqlSettings.DataSourceReplicas.length === 0) {
|
||||
dataSourceReplicas = 'none';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
|
||||
<div className='banner'>
|
||||
<div className='banner__content'>
|
||||
<h4 className='banner__heading'>
|
||||
<FormattedMessage
|
||||
id='admin.sql.noteTitle'
|
||||
defaultMessage='Note:'
|
||||
/>
|
||||
</h4>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='admin.sql.noteDescription'
|
||||
defaultMessage='Changing properties in this section will require a server restart before taking effect.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.sql.title'
|
||||
defaultMessage='SQL Settings'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='DriverName'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sql.driverName'
|
||||
defaultMessage='Driver Name:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<p className='help-text'>{this.props.config.SqlSettings.DriverName}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='DataSource'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sql.dataSource'
|
||||
defaultMessage='Data Source:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<p className='help-text'>{dataSource}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='DataSourceReplicas'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sql.replicas'
|
||||
defaultMessage='Data Source Replicas:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<p className='help-text'>{dataSourceReplicas}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='MaxIdleConns'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sql.maxConnectionsTitle'
|
||||
defaultMessage='Maximum Idle Connections:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='MaxIdleConns'
|
||||
ref='MaxIdleConns'
|
||||
placeholder={formatMessage(holders.maxConnectionsExample)}
|
||||
defaultValue={this.props.config.SqlSettings.MaxIdleConns}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.sql.maxConnectionsDescription'
|
||||
defaultMessage='Maximum number of idle connections held open to the database.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='MaxOpenConns'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sql.maxOpenTitle'
|
||||
defaultMessage='Maximum Open Connections:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='MaxOpenConns'
|
||||
ref='MaxOpenConns'
|
||||
placeholder={formatMessage(holders.maxOpenExample)}
|
||||
defaultValue={this.props.config.SqlSettings.MaxOpenConns}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.sql.maxOpenDescription'
|
||||
defaultMessage='Maximum number of open connections held open to the database.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='AtRestEncryptKey'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sql.keyTitle'
|
||||
defaultMessage='At Rest Encrypt Key:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='AtRestEncryptKey'
|
||||
ref='AtRestEncryptKey'
|
||||
placeholder={formatMessage(holders.keyExample)}
|
||||
defaultValue={this.props.config.SqlSettings.AtRestEncryptKey}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.sql.keyDescription'
|
||||
defaultMessage='32-character salt available to encrypt and decrypt sensitive fields in database.'
|
||||
/>
|
||||
</p>
|
||||
<div className='help-text'>
|
||||
<button
|
||||
className='btn btn-default'
|
||||
onClick={this.handleGenerate}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sql.regenerate'
|
||||
defaultMessage='Re-Generate'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='Trace'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sql.traceTitle'
|
||||
defaultMessage='Trace: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='Trace'
|
||||
value='true'
|
||||
ref='Trace'
|
||||
defaultChecked={this.props.config.SqlSettings.Trace}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.sql.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='Trace'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.SqlSettings.Trace}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.sql.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.sql.traceDescription'
|
||||
defaultMessage='(Development Mode) When true, executing SQL statements are written to the log.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
{serverError}
|
||||
<button
|
||||
disabled={!this.state.saveNeeded}
|
||||
type='submit'
|
||||
className={saveClass}
|
||||
onClick={this.handleSubmit}
|
||||
id='save-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.sql.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SqlSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(SqlSettings);
|
||||
420
webapp/components/admin_console/team_settings.jsx
Обычный файл
420
webapp/components/admin_console/team_settings.jsx
Обычный файл
@@ -0,0 +1,420 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
siteNameExample: {
|
||||
id: 'admin.team.siteNameExample',
|
||||
defaultMessage: 'Ex "Mattermost"'
|
||||
},
|
||||
maxUsersExample: {
|
||||
id: 'admin.team.maxUsersExample',
|
||||
defaultMessage: 'Ex "25"'
|
||||
},
|
||||
restrictExample: {
|
||||
id: 'admin.team.restrictExample',
|
||||
defaultMessage: 'Ex "corp.mattermost.com, mattermost.org"'
|
||||
},
|
||||
saving: {
|
||||
id: 'admin.team.saving',
|
||||
defaultMessage: 'Saving Config...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class TeamSettings extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
saveNeeded: false,
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
handleChange() {
|
||||
var s = {saveNeeded: true, serverError: this.state.serverError};
|
||||
this.setState(s);
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
$('#save-button').button('loading');
|
||||
|
||||
var config = this.props.config;
|
||||
config.TeamSettings.SiteName = ReactDOM.findDOMNode(this.refs.SiteName).value.trim();
|
||||
config.TeamSettings.RestrictCreationToDomains = ReactDOM.findDOMNode(this.refs.RestrictCreationToDomains).value.trim();
|
||||
config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked;
|
||||
config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked;
|
||||
config.TeamSettings.RestrictTeamNames = ReactDOM.findDOMNode(this.refs.RestrictTeamNames).checked;
|
||||
config.TeamSettings.EnableTeamListing = ReactDOM.findDOMNode(this.refs.EnableTeamListing).checked;
|
||||
|
||||
var MaxUsersPerTeam = 50;
|
||||
if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) {
|
||||
MaxUsersPerTeam = parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10);
|
||||
}
|
||||
config.TeamSettings.MaxUsersPerTeam = MaxUsersPerTeam;
|
||||
ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value = MaxUsersPerTeam;
|
||||
|
||||
Client.saveConfig(
|
||||
config,
|
||||
() => {
|
||||
AsyncClient.getConfig();
|
||||
this.setState({
|
||||
serverError: null,
|
||||
saveNeeded: false
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
serverError: err.message,
|
||||
saveNeeded: true
|
||||
});
|
||||
$('#save-button').button('reset');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var saveClass = 'btn';
|
||||
if (this.state.saveNeeded) {
|
||||
saveClass = 'btn btn-primary';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.team.title'
|
||||
defaultMessage='Team Settings'
|
||||
/>
|
||||
</h3>
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='SiteName'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.team.siteNameTitle'
|
||||
defaultMessage='Site Name:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='SiteName'
|
||||
ref='SiteName'
|
||||
placeholder={formatMessage(holders.siteNameExample)}
|
||||
defaultValue={this.props.config.TeamSettings.SiteName}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.team.siteNameDescription'
|
||||
defaultMessage='Name of service shown in login screens and UI.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='MaxUsersPerTeam'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.team.maxUsersTitle'
|
||||
defaultMessage='Max Users Per Team:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='MaxUsersPerTeam'
|
||||
ref='MaxUsersPerTeam'
|
||||
placeholder={formatMessage(holders.maxUsersExample)}
|
||||
defaultValue={this.props.config.TeamSettings.MaxUsersPerTeam}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.team.maxUsersDescription'
|
||||
defaultMessage='Maximum total number of users per team, including both active and inactive users.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableTeamCreation'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.team.teamCreationTitle'
|
||||
defaultMessage='Enable Team Creation: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableTeamCreation'
|
||||
value='true'
|
||||
ref='EnableTeamCreation'
|
||||
defaultChecked={this.props.config.TeamSettings.EnableTeamCreation}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.team.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableTeamCreation'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.TeamSettings.EnableTeamCreation}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.team.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.team.teamCreationDescription'
|
||||
defaultMessage='When false, the ability to create teams is disabled. The create team button displays error when pressed.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableUserCreation'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.team.userCreationTitle'
|
||||
defaultMessage='Enable User Creation: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableUserCreation'
|
||||
value='true'
|
||||
ref='EnableUserCreation'
|
||||
defaultChecked={this.props.config.TeamSettings.EnableUserCreation}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.team.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableUserCreation'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.TeamSettings.EnableUserCreation}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.team.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.team.userCreationDescription'
|
||||
defaultMessage='When false, the ability to create accounts is disabled. The create account button displays error when pressed.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='RestrictCreationToDomains'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.team.restrictTitle'
|
||||
defaultMessage='Restrict Creation To Domains:'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
id='RestrictCreationToDomains'
|
||||
ref='RestrictCreationToDomains'
|
||||
placeholder={formatMessage(holders.restrictExample)}
|
||||
defaultValue={this.props.config.TeamSettings.RestrictCreationToDomains}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.team.restrictDescription'
|
||||
defaultMessage='Teams and user accounts can only be created from a specific domain (e.g. "mattermost.org") or list of comma-separated domains (e.g. "corp.mattermost.com, mattermost.org").'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='RestrictTeamNames'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.team.restrictNameTitle'
|
||||
defaultMessage='Restrict Team Names: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='RestrictTeamNames'
|
||||
value='true'
|
||||
ref='RestrictTeamNames'
|
||||
defaultChecked={this.props.config.TeamSettings.RestrictTeamNames}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.team.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='RestrictTeamNames'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.TeamSettings.RestrictTeamNames}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.team.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.team.restrictNameDesc'
|
||||
defaultMessage='When true, You cannot create a team name with reserved words like www, admin, support, test, channel, etc'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='EnableTeamListing'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.team.dirTitle'
|
||||
defaultMessage='Enable Team Directory: '
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableTeamListing'
|
||||
value='true'
|
||||
ref='EnableTeamListing'
|
||||
defaultChecked={this.props.config.TeamSettings.EnableTeamListing}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.team.true'
|
||||
defaultMessage='true'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
name='EnableTeamListing'
|
||||
value='false'
|
||||
defaultChecked={!this.props.config.TeamSettings.EnableTeamListing}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='admin.team.false'
|
||||
defaultMessage='false'
|
||||
/>
|
||||
</label>
|
||||
<p className='help-text'>
|
||||
<FormattedMessage
|
||||
id='admin.team.dirDesc'
|
||||
defaultMessage='When true, teams that are configured to show in team directory will show on main page inplace of creating a new team.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='form-group'>
|
||||
<div className='col-sm-12'>
|
||||
{serverError}
|
||||
<button
|
||||
disabled={!this.state.saveNeeded}
|
||||
type='submit'
|
||||
className={saveClass}
|
||||
onClick={this.handleSubmit}
|
||||
id='save-button'
|
||||
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ' + formatMessage(holders.saving)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.team.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamSettings.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
config: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(TeamSettings);
|
||||
188
webapp/components/admin_console/team_users.jsx
Обычный файл
188
webapp/components/admin_console/team_users.jsx
Обычный файл
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import LoadingScreen from '../loading_screen.jsx';
|
||||
import UserItem from './user_item.jsx';
|
||||
import ResetPasswordModal from './reset_password_modal.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class UserList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getTeamProfiles = this.getTeamProfiles.bind(this);
|
||||
this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this);
|
||||
this.doPasswordReset = this.doPasswordReset.bind(this);
|
||||
this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
|
||||
this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
teamId: props.team.id,
|
||||
users: null,
|
||||
serverError: null,
|
||||
showPasswordModal: false,
|
||||
user: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getCurrentTeamProfiles();
|
||||
}
|
||||
|
||||
getCurrentTeamProfiles() {
|
||||
this.getTeamProfiles(this.props.team.id);
|
||||
}
|
||||
|
||||
getTeamProfiles(teamId) {
|
||||
Client.getProfilesForTeam(
|
||||
teamId,
|
||||
(users) => {
|
||||
var memberList = [];
|
||||
for (var id in users) {
|
||||
if (users.hasOwnProperty(id)) {
|
||||
memberList.push(users[id]);
|
||||
}
|
||||
}
|
||||
|
||||
memberList.sort((a, b) => {
|
||||
if (a.username < b.username) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.username > b.username) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
teamId: this.state.teamId,
|
||||
users: memberList,
|
||||
serverError: this.state.serverError,
|
||||
showPasswordModal: this.state.showPasswordModal,
|
||||
user: this.state.user
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
teamId: this.state.teamId,
|
||||
users: null,
|
||||
serverError: err.message,
|
||||
showPasswordModal: this.state.showPasswordModal,
|
||||
user: this.state.user
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
doPasswordReset(user) {
|
||||
this.setState({
|
||||
teamId: this.state.teamId,
|
||||
users: this.state.users,
|
||||
serverError: this.state.serverError,
|
||||
showPasswordModal: true,
|
||||
user
|
||||
});
|
||||
}
|
||||
|
||||
doPasswordResetDismiss() {
|
||||
this.setState({
|
||||
teamId: this.state.teamId,
|
||||
users: this.state.users,
|
||||
serverError: this.state.serverError,
|
||||
showPasswordModal: false,
|
||||
user: null
|
||||
});
|
||||
}
|
||||
|
||||
doPasswordResetSubmit() {
|
||||
this.setState({
|
||||
teamId: this.state.teamId,
|
||||
users: this.state.users,
|
||||
serverError: this.state.serverError,
|
||||
showPasswordModal: false,
|
||||
user: null
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
this.getTeamProfiles(newProps.team.id);
|
||||
}
|
||||
|
||||
render() {
|
||||
var serverError = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
if (this.state.users == null) {
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.userList.title'
|
||||
defaultMessage='Users for {team}'
|
||||
values={{
|
||||
team: this.props.team.name
|
||||
}}
|
||||
/>
|
||||
</h3>
|
||||
{serverError}
|
||||
<LoadingScreen/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var memberList = this.state.users.map((user) => {
|
||||
return (
|
||||
<UserItem
|
||||
key={'user_' + user.id}
|
||||
user={user}
|
||||
refreshProfiles={this.getCurrentTeamProfiles}
|
||||
doPasswordReset={this.doPasswordReset}
|
||||
/>);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.userList.title2'
|
||||
defaultMessage='Users for {team} ({count})'
|
||||
values={{
|
||||
team: this.props.team.name,
|
||||
count: this.state.users.length
|
||||
}}
|
||||
/>
|
||||
</h3>
|
||||
{serverError}
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
>
|
||||
<table className='more-modal__list member-list-holder'>
|
||||
<tbody>
|
||||
{memberList}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<ResetPasswordModal
|
||||
user={this.state.user}
|
||||
show={this.state.showPasswordModal}
|
||||
team={this.props.team}
|
||||
onModalSubmit={this.doPasswordResetSubmit}
|
||||
onModalDismissed={this.doPasswordResetDismiss}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserList.propTypes = {
|
||||
team: React.PropTypes.object
|
||||
};
|
||||
423
webapp/components/admin_console/user_item.jsx
Обычный файл
423
webapp/components/admin_console/user_item.jsx
Обычный файл
@@ -0,0 +1,423 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import ConfirmModal from '../confirm_modal.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class UserItem extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleMakeMember = this.handleMakeMember.bind(this);
|
||||
this.handleMakeActive = this.handleMakeActive.bind(this);
|
||||
this.handleMakeNotActive = this.handleMakeNotActive.bind(this);
|
||||
this.handleMakeAdmin = this.handleMakeAdmin.bind(this);
|
||||
this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this);
|
||||
this.handleResetPassword = this.handleResetPassword.bind(this);
|
||||
this.handleDemote = this.handleDemote.bind(this);
|
||||
this.handleDemoteSubmit = this.handleDemoteSubmit.bind(this);
|
||||
this.handleDemoteCancel = this.handleDemoteCancel.bind(this);
|
||||
|
||||
this.state = {
|
||||
serverError: null,
|
||||
showDemoteModal: false,
|
||||
user: null,
|
||||
role: null
|
||||
};
|
||||
}
|
||||
|
||||
handleMakeMember(e) {
|
||||
e.preventDefault();
|
||||
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,
|
||||
() => {
|
||||
this.props.refreshProfiles();
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleMakeActive(e) {
|
||||
e.preventDefault();
|
||||
Client.updateActive(this.props.user.id, true,
|
||||
() => {
|
||||
this.props.refreshProfiles();
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleMakeNotActive(e) {
|
||||
e.preventDefault();
|
||||
Client.updateActive(this.props.user.id, false,
|
||||
() => {
|
||||
this.props.refreshProfiles();
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleMakeAdmin(e) {
|
||||
e.preventDefault();
|
||||
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,
|
||||
() => {
|
||||
this.props.refreshProfiles();
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleMakeSystemAdmin(e) {
|
||||
e.preventDefault();
|
||||
const data = {
|
||||
user_id: this.props.user.id,
|
||||
new_roles: 'system_admin'
|
||||
};
|
||||
|
||||
Client.updateRoles(data,
|
||||
() => {
|
||||
this.props.refreshProfiles();
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleResetPassword(e) {
|
||||
e.preventDefault();
|
||||
this.props.doPasswordReset(this.props.user);
|
||||
}
|
||||
|
||||
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: this.state.role
|
||||
};
|
||||
|
||||
Client.updateRoles(data,
|
||||
() => {
|
||||
this.setState({
|
||||
serverError: null,
|
||||
showDemoteModal: false,
|
||||
user: null,
|
||||
role: null
|
||||
});
|
||||
|
||||
const teamUrl = TeamStore.getCurrentTeamUrl();
|
||||
if (teamUrl) {
|
||||
window.location.href = teamUrl;
|
||||
} else {
|
||||
window.location.href = '/';
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
serverError: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let serverError = null;
|
||||
if (this.state.serverError) {
|
||||
serverError = (
|
||||
<div className='has-error'>
|
||||
<label className='has-error control-label'>{this.state.serverError}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const user = this.props.user;
|
||||
let currentRoles = (
|
||||
<FormattedMessage
|
||||
id='admin.user_item.member'
|
||||
defaultMessage='Member'
|
||||
/>
|
||||
);
|
||||
if (user.roles.length > 0) {
|
||||
if (Utils.isSystemAdmin(user.roles)) {
|
||||
currentRoles = (
|
||||
<FormattedMessage
|
||||
id='admin.user_item.sysAdmin'
|
||||
defaultMessage='System Admin'
|
||||
/>
|
||||
);
|
||||
} else if (Utils.isAdmin(user.roles)) {
|
||||
currentRoles = (
|
||||
<FormattedMessage
|
||||
id='admin.user_item.teamAdmin'
|
||||
defaultMessage='Team Admin'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
const email = user.email;
|
||||
let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin';
|
||||
let showMakeAdmin = user.roles === '' || user.roles === 'system_admin';
|
||||
let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin';
|
||||
let showMakeActive = false;
|
||||
let showMakeNotActive = user.roles !== 'system_admin';
|
||||
|
||||
if (user.delete_at > 0) {
|
||||
currentRoles = (
|
||||
<FormattedMessage
|
||||
id='admin.user_item.inactive'
|
||||
defaultMessage='Inactive'
|
||||
/>
|
||||
);
|
||||
showMakeMember = false;
|
||||
showMakeAdmin = false;
|
||||
showMakeSystemAdmin = false;
|
||||
showMakeActive = true;
|
||||
showMakeNotActive = false;
|
||||
}
|
||||
|
||||
let makeSystemAdmin = null;
|
||||
if (showMakeSystemAdmin) {
|
||||
makeSystemAdmin = (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.handleMakeSystemAdmin}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.makeSysAdmin'
|
||||
defaultMessage='Make System Admin'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
let makeAdmin = null;
|
||||
if (showMakeAdmin) {
|
||||
makeAdmin = (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.handleMakeAdmin}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.makeTeamAdmin'
|
||||
defaultMessage='Make Team Admin'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
let makeMember = null;
|
||||
if (showMakeMember) {
|
||||
makeMember = (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.handleMakeMember}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.makeMember'
|
||||
defaultMessage='Make Member'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
let makeActive = null;
|
||||
if (showMakeActive) {
|
||||
makeActive = (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.handleMakeActive}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.makeActive'
|
||||
defaultMessage='Make Active'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
let makeNotActive = null;
|
||||
if (showMakeNotActive) {
|
||||
makeNotActive = (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.handleMakeNotActive}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.makeInactive'
|
||||
defaultMessage='Make Inactive'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
const me = UserStore.getCurrentUser();
|
||||
let makeDemoteModal = null;
|
||||
if (this.props.user.id === me.id) {
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='admin.user_item.confirmDemoteRoleTitle'
|
||||
defaultMessage='Confirm demotion from System Admin role'
|
||||
/>
|
||||
);
|
||||
|
||||
const message = (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.confirmDemoteDescription'
|
||||
defaultMessage="If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you\'ll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command."
|
||||
/>
|
||||
<br/>
|
||||
<br/>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.confirmDemotionCmd'
|
||||
defaultMessage='platform -assign_role -team_name="yourteam" -email="name@yourcompany.com" -role="system_admin"'
|
||||
/>
|
||||
{serverError}
|
||||
</div>
|
||||
);
|
||||
|
||||
const confirmButton = (
|
||||
<FormattedMessage
|
||||
id='admin.user_item.confirmDemotion'
|
||||
defaultMessage='Confirm Demotion'
|
||||
/>
|
||||
);
|
||||
|
||||
makeDemoteModal = (
|
||||
<ConfirmModal
|
||||
show={this.state.showDemoteModal}
|
||||
title={title}
|
||||
message={message}
|
||||
confirmButton={confirmButton}
|
||||
onConfirm={this.handleDemoteSubmit}
|
||||
onCancel={this.handleDemoteCancel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className='more-modal__row'>
|
||||
<img
|
||||
className='more-modal__image pull-left'
|
||||
src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
|
||||
height='36'
|
||||
width='36'
|
||||
/>
|
||||
<span className='more-modal__name'>{Utils.getDisplayName(user)}</span>
|
||||
<span className='more-modal__description'>{email}</span>
|
||||
<div className='dropdown member-drop'>
|
||||
<a
|
||||
href='#'
|
||||
className='dropdown-toggle theme'
|
||||
type='button'
|
||||
data-toggle='dropdown'
|
||||
aria-expanded='true'
|
||||
>
|
||||
<span>{currentRoles} </span>
|
||||
<span className='caret'></span>
|
||||
</a>
|
||||
<ul
|
||||
className='dropdown-menu member-menu'
|
||||
role='menu'
|
||||
>
|
||||
{makeAdmin}
|
||||
{makeMember}
|
||||
{makeActive}
|
||||
{makeNotActive}
|
||||
{makeSystemAdmin}
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.handleResetPassword}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='admin.user_item.resetPwd'
|
||||
defaultMessage='Reset Password'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{makeDemoteModal}
|
||||
{serverError}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UserItem.propTypes = {
|
||||
user: React.PropTypes.object.isRequired,
|
||||
refreshProfiles: React.PropTypes.func.isRequired,
|
||||
doPasswordReset: React.PropTypes.func.isRequired
|
||||
};
|
||||
81
webapp/components/analytics/doughnut_chart.jsx
Обычный файл
81
webapp/components/analytics/doughnut_chart.jsx
Обычный файл
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import Chart from 'chart.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class DoughnutChart 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.refs.canvas);
|
||||
var ctx = el.getContext('2d');
|
||||
this.chart = new Chart(ctx).Doughnut(props.data, props.options || {}); //eslint-disable-line new-cap
|
||||
}
|
||||
|
||||
render() {
|
||||
let content;
|
||||
if (this.props.data == null) {
|
||||
content = (
|
||||
<FormattedMessage
|
||||
id='analytics.chart.loading'
|
||||
defaultMessage='Loading...'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<canvas
|
||||
ref='canvas'
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='col-sm-6'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className='content'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DoughnutChart.propTypes = {
|
||||
title: React.PropTypes.node,
|
||||
width: React.PropTypes.string,
|
||||
height: React.PropTypes.string,
|
||||
data: React.PropTypes.array,
|
||||
options: React.PropTypes.object
|
||||
};
|
||||
94
webapp/components/analytics/line_chart.jsx
Обычный файл
94
webapp/components/analytics/line_chart.jsx
Обычный файл
@@ -0,0 +1,94 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import Chart from 'chart.js';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class LineChart extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.initChart = this.initChart.bind(this);
|
||||
this.chart = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
this.initChart();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
initChart() {
|
||||
if (!this.refs.canvas) {
|
||||
return;
|
||||
}
|
||||
var el = ReactDOM.findDOMNode(this.refs.canvas);
|
||||
var ctx = el.getContext('2d');
|
||||
this.chart = new Chart(ctx).Line(this.props.data, this.props.options || {}); //eslint-disable-line new-cap
|
||||
}
|
||||
|
||||
render() {
|
||||
let content;
|
||||
if (this.props.data == null) {
|
||||
content = (
|
||||
<FormattedMessage
|
||||
id='analytics.chart.loading'
|
||||
defaultMessage='Loading...'
|
||||
/>
|
||||
);
|
||||
} else if (this.props.data.labels.length === 0) {
|
||||
content = (
|
||||
<h5>
|
||||
<FormattedMessage
|
||||
id='analytics.chart.meaningful'
|
||||
defaultMessage='Not enough data for a meaningful representation.'
|
||||
/>
|
||||
</h5>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<canvas
|
||||
ref='canvas'
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='col-sm-12'>
|
||||
<div className='total-count by-day'>
|
||||
<div className='title'>
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className='content'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LineChart.propTypes = {
|
||||
title: React.PropTypes.node.isRequired,
|
||||
width: React.PropTypes.string.isRequired,
|
||||
height: React.PropTypes.string.isRequired,
|
||||
data: React.PropTypes.object,
|
||||
options: React.PropTypes.object
|
||||
};
|
||||
|
||||
35
webapp/components/analytics/statistic_count.jsx
Обычный файл
35
webapp/components/analytics/statistic_count.jsx
Обычный файл
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class StatisticCount extends React.Component {
|
||||
render() {
|
||||
let loading = (
|
||||
<FormattedMessage
|
||||
id='analytics.chart.loading'
|
||||
defaultMessage='Loading...'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='col-sm-3'>
|
||||
<div className='total-count'>
|
||||
<div className='title'>
|
||||
{this.props.title}
|
||||
<i className={'fa ' + this.props.icon}/>
|
||||
</div>
|
||||
<div className='content'>{this.props.count == null ? loading : this.props.count}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StatisticCount.propTypes = {
|
||||
title: React.PropTypes.node.isRequired,
|
||||
icon: React.PropTypes.string.isRequired,
|
||||
count: React.PropTypes.number
|
||||
};
|
||||
348
webapp/components/analytics/system_analytics.jsx
Обычный файл
348
webapp/components/analytics/system_analytics.jsx
Обычный файл
@@ -0,0 +1,348 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import LineChart from './line_chart.jsx';
|
||||
import DoughnutChart from './doughnut_chart.jsx';
|
||||
import StatisticCount from './statistic_count.jsx';
|
||||
|
||||
import AnalyticsStore from 'stores/analytics_store.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const StatTypes = Constants.StatTypes;
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
analyticsPublicChannels: {
|
||||
id: 'analytics.system.publicChannels',
|
||||
defaultMessage: 'Public Channels'
|
||||
},
|
||||
analyticsPrivateGroups: {
|
||||
id: 'analytics.system.privateGroups',
|
||||
defaultMessage: 'Private Groups'
|
||||
},
|
||||
analyticsFilePosts: {
|
||||
id: 'analytics.system.totalFilePosts',
|
||||
defaultMessage: 'Posts with Files'
|
||||
},
|
||||
analyticsHashtagPosts: {
|
||||
id: 'analytics.system.totalHashtagPosts',
|
||||
defaultMessage: 'Posts with Hashtags'
|
||||
},
|
||||
analyticsTextPosts: {
|
||||
id: 'analytics.system.textPosts',
|
||||
defaultMessage: 'Posts with Text-only'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class SystemAnalytics extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
|
||||
this.state = {stats: AnalyticsStore.getAllSystem()};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AnalyticsStore.addChangeListener(this.onChange);
|
||||
|
||||
AsyncClient.getStandardAnalytics();
|
||||
AsyncClient.getPostsPerDayAnalytics();
|
||||
AsyncClient.getUsersPerDayAnalytics();
|
||||
|
||||
if (global.window.mm_license.IsLicensed === 'true') {
|
||||
AsyncClient.getAdvancedAnalytics();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AnalyticsStore.removeChangeListener(this.onChange);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.setState({stats: AnalyticsStore.getAllSystem()});
|
||||
}
|
||||
|
||||
render() {
|
||||
const stats = this.state.stats;
|
||||
|
||||
let advancedCounts;
|
||||
let advancedGraphs;
|
||||
if (global.window.mm_license.IsLicensed === 'true') {
|
||||
advancedCounts = (
|
||||
<div className='row'>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalSessions'
|
||||
defaultMessage='Total Sessions'
|
||||
/>
|
||||
}
|
||||
icon='fa-signal'
|
||||
count={stats[StatTypes.TOTAL_SESSIONS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalCommands'
|
||||
defaultMessage='Total Commands'
|
||||
/>
|
||||
}
|
||||
icon='fa-terminal'
|
||||
count={stats[StatTypes.TOTAL_COMMANDS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalIncomingWebhooks'
|
||||
defaultMessage='Incoming Webhooks'
|
||||
/>
|
||||
}
|
||||
icon='fa-arrow-down'
|
||||
count={stats[StatTypes.TOTAL_IHOOKS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalOutgoingWebhooks'
|
||||
defaultMessage='Outgoing Webhooks'
|
||||
/>
|
||||
}
|
||||
icon='fa-arrow-up'
|
||||
count={stats[StatTypes.TOTAL_OHOOKS]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const channelTypeData = formatChannelDoughtnutData(stats[StatTypes.TOTAL_PUBLIC_CHANNELS], stats[StatTypes.TOTAL_PRIVATE_GROUPS], this.props.intl);
|
||||
const postTypeData = formatPostDoughtnutData(stats[StatTypes.TOTAL_FILE_POSTS], stats[StatTypes.TOTAL_HASHTAG_POSTS], stats[StatTypes.TOTAL_POSTS], this.props.intl);
|
||||
|
||||
advancedGraphs = (
|
||||
<div className='row'>
|
||||
<DoughnutChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.channelTypes'
|
||||
defaultMessage='Channel Types'
|
||||
/>
|
||||
}
|
||||
data={channelTypeData}
|
||||
width='300'
|
||||
height='225'
|
||||
/>
|
||||
<DoughnutChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.postTypes'
|
||||
defaultMessage='Posts, Files and Hashtags'
|
||||
/>
|
||||
}
|
||||
data={postTypeData}
|
||||
width='300'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]);
|
||||
const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]);
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed team_statistics'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='analytics.system.title'
|
||||
defaultMessage='System Statistics'
|
||||
/>
|
||||
</h3>
|
||||
<div className='row'>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalUsers'
|
||||
defaultMessage='Total Users'
|
||||
/>
|
||||
}
|
||||
icon='fa-user'
|
||||
count={stats[StatTypes.TOTAL_USERS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalTeams'
|
||||
defaultMessage='Total Teams'
|
||||
/>
|
||||
}
|
||||
icon='fa-users'
|
||||
count={stats[StatTypes.TOTAL_TEAMS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalPosts'
|
||||
defaultMessage='Total Posts'
|
||||
/>
|
||||
}
|
||||
icon='fa-comment'
|
||||
count={stats[StatTypes.TOTAL_POSTS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalChannels'
|
||||
defaultMessage='Total Channels'
|
||||
/>
|
||||
}
|
||||
icon='fa-globe'
|
||||
count={stats[StatTypes.TOTAL_PUBLIC_CHANNELS] + stats[StatTypes.TOTAL_PRIVATE_GROUPS]}
|
||||
/>
|
||||
</div>
|
||||
{advancedCounts}
|
||||
{advancedGraphs}
|
||||
<div className='row'>
|
||||
<LineChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.totalPosts'
|
||||
defaultMessage='Total Posts'
|
||||
/>
|
||||
}
|
||||
data={postCountsDay}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<LineChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.system.activeUsers'
|
||||
defaultMessage='Active Users With Posts'
|
||||
/>
|
||||
}
|
||||
data={userCountsWithPostsDay}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SystemAnalytics.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
team: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(SystemAnalytics);
|
||||
|
||||
export function formatChannelDoughtnutData(totalPublic, totalPrivate, intl) {
|
||||
const {formatMessage} = intl;
|
||||
const channelTypeData = [
|
||||
{
|
||||
value: totalPublic,
|
||||
color: '#46BFBD',
|
||||
highlight: '#5AD3D1',
|
||||
label: formatMessage(holders.analyticsPublicChannels)
|
||||
},
|
||||
{
|
||||
value: totalPrivate,
|
||||
color: '#FDB45C',
|
||||
highlight: '#FFC870',
|
||||
label: formatMessage(holders.analyticsPrivateGroups)
|
||||
}
|
||||
];
|
||||
|
||||
return channelTypeData;
|
||||
}
|
||||
|
||||
export function formatPostDoughtnutData(filePosts, hashtagPosts, totalPosts, intl) {
|
||||
const {formatMessage} = intl;
|
||||
const postTypeData = [
|
||||
{
|
||||
value: filePosts,
|
||||
color: '#46BFBD',
|
||||
highlight: '#5AD3D1',
|
||||
label: formatMessage(holders.analyticsFilePosts)
|
||||
},
|
||||
{
|
||||
value: hashtagPosts,
|
||||
color: '#F7464A',
|
||||
highlight: '#FF5A5E',
|
||||
label: formatMessage(holders.analyticsHashtagPosts)
|
||||
},
|
||||
{
|
||||
value: totalPosts - filePosts - hashtagPosts,
|
||||
color: '#FDB45C',
|
||||
highlight: '#FFC870',
|
||||
label: formatMessage(holders.analyticsTextPosts)
|
||||
}
|
||||
];
|
||||
|
||||
return postTypeData;
|
||||
}
|
||||
|
||||
export function formatPostsPerDayData(data) {
|
||||
var chartData = {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
fillColor: 'rgba(151,187,205,0.2)',
|
||||
strokeColor: 'rgba(151,187,205,1)',
|
||||
pointColor: 'rgba(151,187,205,1)',
|
||||
pointStrokeColor: '#fff',
|
||||
pointHighlightFill: '#fff',
|
||||
pointHighlightStroke: 'rgba(151,187,205,1)',
|
||||
data: []
|
||||
}]
|
||||
};
|
||||
|
||||
for (var index in data) {
|
||||
if (data[index]) {
|
||||
var row = data[index];
|
||||
chartData.labels.push(row.name);
|
||||
chartData.datasets[0].data.push(row.value);
|
||||
}
|
||||
}
|
||||
|
||||
return chartData;
|
||||
}
|
||||
|
||||
export function formatUsersWithPostsPerDayData(data) {
|
||||
var chartData = {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
fillColor: 'rgba(151,187,205,0.2)',
|
||||
strokeColor: 'rgba(151,187,205,1)',
|
||||
pointColor: 'rgba(151,187,205,1)',
|
||||
pointStrokeColor: '#fff',
|
||||
pointHighlightFill: '#fff',
|
||||
pointHighlightStroke: 'rgba(151,187,205,1)',
|
||||
data: []
|
||||
}]
|
||||
};
|
||||
|
||||
for (var index in data) {
|
||||
if (data[index]) {
|
||||
var row = data[index];
|
||||
chartData.labels.push(row.name);
|
||||
chartData.datasets[0].data.push(row.value);
|
||||
}
|
||||
}
|
||||
|
||||
return chartData;
|
||||
}
|
||||
61
webapp/components/analytics/table_chart.jsx
Обычный файл
61
webapp/components/analytics/table_chart.jsx
Обычный файл
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class TableChart extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className='col-sm-6'>
|
||||
<div className='total-count recent-active-users'>
|
||||
<div className='title'>
|
||||
{this.props.title}
|
||||
</div>
|
||||
<div className='content'>
|
||||
<table>
|
||||
<tbody>
|
||||
{
|
||||
this.props.data.map((item) => {
|
||||
const tooltip = (
|
||||
<Tooltip id={'tip-table-entry-' + item.name}>
|
||||
{item.tip}
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<tr key={'table-entry-' + item.name}>
|
||||
<td>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='top'
|
||||
overlay={tooltip}
|
||||
>
|
||||
<time>
|
||||
{item.name}
|
||||
</time>
|
||||
</OverlayTrigger>
|
||||
</td>
|
||||
<td>
|
||||
{item.value}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TableChart.propTypes = {
|
||||
title: React.PropTypes.node,
|
||||
data: React.PropTypes.array
|
||||
};
|
||||
237
webapp/components/analytics/team_analytics.jsx
Обычный файл
237
webapp/components/analytics/team_analytics.jsx
Обычный файл
@@ -0,0 +1,237 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import LineChart from './line_chart.jsx';
|
||||
import StatisticCount from './statistic_count.jsx';
|
||||
import TableChart from './table_chart.jsx';
|
||||
|
||||
import AnalyticsStore from 'stores/analytics_store.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const StatTypes = Constants.StatTypes;
|
||||
|
||||
import {formatPostsPerDayData, formatUsersWithPostsPerDayData} from './system_analytics.jsx';
|
||||
import {injectIntl, intlShape, FormattedMessage, FormattedDate} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class TeamAnalytics extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
|
||||
this.state = {stats: AnalyticsStore.getAllTeam(this.props.team.id)};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
AnalyticsStore.addChangeListener(this.onChange);
|
||||
|
||||
this.getData(this.props.team.id);
|
||||
}
|
||||
|
||||
getData(id) {
|
||||
AsyncClient.getStandardAnalytics(id);
|
||||
AsyncClient.getPostsPerDayAnalytics(id);
|
||||
AsyncClient.getUsersPerDayAnalytics(id);
|
||||
AsyncClient.getRecentAndNewUsersAnalytics(id);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
AnalyticsStore.removeChangeListener(this.onChange);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.getData(nextProps.team.id);
|
||||
this.setState({stats: AnalyticsStore.getAllTeam(nextProps.team.id)});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(nextState.stats, this.state.stats)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextProps.team, this.props.team)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.setState({stats: AnalyticsStore.getAllTeam(this.props.team.id)});
|
||||
}
|
||||
|
||||
render() {
|
||||
const stats = this.state.stats;
|
||||
const postCountsDay = formatPostsPerDayData(stats[StatTypes.POST_PER_DAY]);
|
||||
const userCountsWithPostsDay = formatUsersWithPostsPerDayData(stats[StatTypes.USERS_WITH_POSTS_PER_DAY]);
|
||||
const recentActiveUsers = formatRecentUsersData(stats[StatTypes.RECENTLY_ACTIVE_USERS]);
|
||||
const newlyCreatedUsers = formatNewUsersData(stats[StatTypes.NEWLY_CREATED_USERS]);
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed team_statistics'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='analytics.team.title'
|
||||
defaultMessage='Team Statistics for {team}'
|
||||
values={{
|
||||
team: this.props.team.name
|
||||
}}
|
||||
/>
|
||||
</h3>
|
||||
<div className='row'>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.totalUsers'
|
||||
defaultMessage='Total Users'
|
||||
/>
|
||||
}
|
||||
icon='fa-user'
|
||||
count={stats[StatTypes.TOTAL_USERS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.publicChannels'
|
||||
defaultMessage='Public Channels'
|
||||
/>
|
||||
}
|
||||
icon='fa-users'
|
||||
count={stats[StatTypes.TOTAL_PUBLIC_CHANNELS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.privateGroups'
|
||||
defaultMessage='Private Groups'
|
||||
/>
|
||||
}
|
||||
icon='fa-globe'
|
||||
count={stats[StatTypes.TOTAL_PRIVATE_GROUPS]}
|
||||
/>
|
||||
<StatisticCount
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.totalPosts'
|
||||
defaultMessage='Total Posts'
|
||||
/>
|
||||
}
|
||||
icon='fa-comment'
|
||||
count={stats[StatTypes.TOTAL_POSTS]}
|
||||
/>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<LineChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.totalPosts'
|
||||
defaultMessage='Total Posts'
|
||||
/>
|
||||
}
|
||||
data={postCountsDay}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<LineChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.activeUsers'
|
||||
defaultMessage='Active Users With Posts'
|
||||
/>
|
||||
}
|
||||
data={userCountsWithPostsDay}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<TableChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.activeUsers'
|
||||
defaultMessage='Recent Active Users'
|
||||
/>
|
||||
}
|
||||
data={recentActiveUsers}
|
||||
/>
|
||||
<TableChart
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='analytics.team.newlyCreated'
|
||||
defaultMessage='Newly Created Users'
|
||||
/>
|
||||
}
|
||||
data={newlyCreatedUsers}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamAnalytics.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
team: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(TeamAnalytics);
|
||||
|
||||
export function formatRecentUsersData(data) {
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const formattedData = data.map((user) => {
|
||||
const item = {};
|
||||
item.name = user.username;
|
||||
item.value = (
|
||||
<FormattedDate
|
||||
value={user.last_activity_at}
|
||||
day='numeric'
|
||||
month='long'
|
||||
year='numeric'
|
||||
hour12={true}
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
/>
|
||||
);
|
||||
item.tip = user.email;
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
return formattedData;
|
||||
}
|
||||
|
||||
export function formatNewUsersData(data) {
|
||||
if (data == null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const formattedData = data.map((user) => {
|
||||
const item = {};
|
||||
item.name = user.username;
|
||||
item.value = (
|
||||
<FormattedDate
|
||||
value={user.create_at}
|
||||
day='numeric'
|
||||
month='long'
|
||||
year='numeric'
|
||||
hour12={true}
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
/>
|
||||
);
|
||||
item.tip = user.email;
|
||||
|
||||
return item;
|
||||
});
|
||||
|
||||
return formattedData;
|
||||
}
|
||||
120
webapp/components/audio_video_preview.jsx
Обычный файл
120
webapp/components/audio_video_preview.jsx
Обычный файл
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import FileInfoPreview from './file_info_preview.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class AudioVideoPreview extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleFileInfoChanged = this.handleFileInfoChanged.bind(this);
|
||||
this.handleLoadError = this.handleLoadError.bind(this);
|
||||
|
||||
this.stop = this.stop.bind(this);
|
||||
|
||||
this.state = {
|
||||
canPlay: true
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.handleFileInfoChanged(this.props.fileInfo);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.refs.source) {
|
||||
$(ReactDOM.findDOMNode(this.refs.source)).one('error', this.handleLoadError);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.fileUrl !== nextProps.fileUrl) {
|
||||
this.handleFileInfoChanged(nextProps.fileInfo);
|
||||
}
|
||||
}
|
||||
|
||||
handleFileInfoChanged(fileInfo) {
|
||||
let video = ReactDOM.findDOMNode(this.refs.video);
|
||||
if (!video) {
|
||||
video = document.createElement('video');
|
||||
}
|
||||
|
||||
const canPlayType = video.canPlayType(fileInfo.mime_type);
|
||||
|
||||
this.setState({
|
||||
canPlay: canPlayType === 'probably' || canPlayType === 'maybe'
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.refs.source) {
|
||||
$(ReactDOM.findDOMNode(this.refs.source)).one('error', this.handleLoadError);
|
||||
}
|
||||
}
|
||||
|
||||
handleLoadError() {
|
||||
this.setState({
|
||||
canPlay: false
|
||||
});
|
||||
}
|
||||
|
||||
stop() {
|
||||
if (this.refs.video) {
|
||||
const video = ReactDOM.findDOMNode(this.refs.video);
|
||||
video.pause();
|
||||
video.currentTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.canPlay) {
|
||||
return (
|
||||
<FileInfoPreview
|
||||
filename={this.props.filename}
|
||||
fileUrl={this.props.fileUrl}
|
||||
fileInfo={this.props.fileInfo}
|
||||
formatMessage={this.props.formatMessage}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let width = Constants.WEB_VIDEO_WIDTH;
|
||||
let height = Constants.WEB_VIDEO_HEIGHT;
|
||||
if (Utils.isMobile()) {
|
||||
width = Constants.MOBILE_VIDEO_WIDTH;
|
||||
height = Constants.MOBILE_VIDEO_HEIGHT;
|
||||
}
|
||||
|
||||
// add a key to the video to prevent React from using an old video source while a new one is loading
|
||||
return (
|
||||
<video
|
||||
key={this.props.filename}
|
||||
ref='video'
|
||||
style={{maxHeight: this.props.maxHeight}}
|
||||
data-setup='{}'
|
||||
controls='controls'
|
||||
width={width}
|
||||
height={height}
|
||||
>
|
||||
<source
|
||||
ref='source'
|
||||
src={this.props.fileUrl}
|
||||
/>
|
||||
</video>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AudioVideoPreview.propTypes = {
|
||||
filename: React.PropTypes.string.isRequired,
|
||||
fileUrl: React.PropTypes.string.isRequired,
|
||||
fileInfo: React.PropTypes.object.isRequired,
|
||||
maxHeight: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.number]).isRequired,
|
||||
formatMessage: React.PropTypes.func.isRequired
|
||||
};
|
||||
626
webapp/components/audit_table.jsx
Обычный файл
626
webapp/components/audit_table.jsx
Обычный файл
@@ -0,0 +1,626 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedDate, FormattedTime} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
sessionRevoked: {
|
||||
id: 'audit_table.sessionRevoked',
|
||||
defaultMessage: 'The session with id {sessionId} was revoked'
|
||||
},
|
||||
channelCreated: {
|
||||
id: 'audit_table.channelCreated',
|
||||
defaultMessage: 'Created the {channelName} channel/group'
|
||||
},
|
||||
establishedDM: {
|
||||
id: 'audit_table.establishedDM',
|
||||
defaultMessage: 'Established a direct message channel with {username}'
|
||||
},
|
||||
nameUpdated: {
|
||||
id: 'audit_table.nameUpdated',
|
||||
defaultMessage: 'Updated the {channelName} channel/group name'
|
||||
},
|
||||
headerUpdated: {
|
||||
id: 'audit_table.headerUpdated',
|
||||
defaultMessage: 'Updated the {channelName} channel/group header'
|
||||
},
|
||||
channelDeleted: {
|
||||
id: 'audit_table.channelDeleted',
|
||||
defaultMessage: 'Deleted the channel/group with the URL {url}'
|
||||
},
|
||||
userAdded: {
|
||||
id: 'audit_table.userAdded',
|
||||
defaultMessage: 'Added {username} to the {channelName} channel/group'
|
||||
},
|
||||
userRemoved: {
|
||||
id: 'audit_table.userRemoved',
|
||||
defaultMessage: 'Removed {username} to the {channelName} channel/group'
|
||||
},
|
||||
attemptedRegisterApp: {
|
||||
id: 'audit_table.attemptedRegisterApp',
|
||||
defaultMessage: 'Attempted to register a new OAuth Application with ID {id}'
|
||||
},
|
||||
attemptedAllowOAuthAccess: {
|
||||
id: 'audit_table.attemptedAllowOAuthAccess',
|
||||
defaultMessage: 'Attempted to allow a new OAuth service access'
|
||||
},
|
||||
successfullOAuthAccess: {
|
||||
id: 'audit_table.successfullOAuthAccess',
|
||||
defaultMessage: 'Successfully gave a new OAuth service access'
|
||||
},
|
||||
failedOAuthAccess: {
|
||||
id: 'audit_table.failedOAuthAccess',
|
||||
defaultMessage: 'Failed to allow a new OAuth service access - the redirect URI did not match the previously registered callback'
|
||||
},
|
||||
attemptedOAuthToken: {
|
||||
id: 'audit_table.attemptedOAuthToken',
|
||||
defaultMessage: 'Attempted to get an OAuth access token'
|
||||
},
|
||||
successfullOAuthToken: {
|
||||
id: 'audit_table.successfullOAuthToken',
|
||||
defaultMessage: 'Successfully added a new OAuth service'
|
||||
},
|
||||
oauthTokenFailed: {
|
||||
id: 'audit_table.oauthTokenFailed',
|
||||
defaultMessage: 'Failed to get an OAuth access token - {token}'
|
||||
},
|
||||
attemptedLogin: {
|
||||
id: 'audit_table.attemptedLogin',
|
||||
defaultMessage: 'Attempted to login'
|
||||
},
|
||||
successfullLogin: {
|
||||
id: 'audit_table.successfullLogin',
|
||||
defaultMessage: 'Successfully logged in'
|
||||
},
|
||||
failedLogin: {
|
||||
id: 'audit_table.failedLogin',
|
||||
defaultMessage: 'FAILED login attempt'
|
||||
},
|
||||
updatePicture: {
|
||||
id: 'audit_table.updatePicture',
|
||||
defaultMessage: 'Updated your profile picture'
|
||||
},
|
||||
updateGeneral: {
|
||||
id: 'audit_table.updateGeneral',
|
||||
defaultMessage: 'Updated the general settings of your account'
|
||||
},
|
||||
attemptedPassword: {
|
||||
id: 'audit_table.attemptedPassword',
|
||||
defaultMessage: 'Attempted to change password'
|
||||
},
|
||||
successfullPassword: {
|
||||
id: 'audit_table.successfullPassword',
|
||||
defaultMessage: 'Successfully changed password'
|
||||
},
|
||||
failedPassword: {
|
||||
id: 'audit_table.failedPassword',
|
||||
defaultMessage: 'Failed to change password - tried to update user password who was logged in through oauth'
|
||||
},
|
||||
updatedRol: {
|
||||
id: 'audit_table.updatedRol',
|
||||
defaultMessage: 'Updated user role(s) to '
|
||||
},
|
||||
member: {
|
||||
id: 'audit_table.member',
|
||||
defaultMessage: 'member'
|
||||
},
|
||||
accountActive: {
|
||||
id: 'audit_table.accountActive',
|
||||
defaultMessage: 'Account made active'
|
||||
},
|
||||
accountInactive: {
|
||||
id: 'audit_table.accountInactive',
|
||||
defaultMessage: 'Account made inactive'
|
||||
},
|
||||
by: {
|
||||
id: 'audit_table.by',
|
||||
defaultMessage: ' by {username}'
|
||||
},
|
||||
byAdmin: {
|
||||
id: 'audit_table.byAdmin',
|
||||
defaultMessage: ' by an admin'
|
||||
},
|
||||
sentEmail: {
|
||||
id: 'audit_table.sentEmail',
|
||||
defaultMessage: 'Sent an email to {email} to reset your password'
|
||||
},
|
||||
attemptedReset: {
|
||||
id: 'audit_table.attemptedReset',
|
||||
defaultMessage: 'Attempted to reset password'
|
||||
},
|
||||
successfullReset: {
|
||||
id: 'audit_table.successfullReset',
|
||||
defaultMessage: 'Successfully reset password'
|
||||
},
|
||||
updateGlobalNotifications: {
|
||||
id: 'audit_table.updateGlobalNotifications',
|
||||
defaultMessage: 'Updated your global notification settings'
|
||||
},
|
||||
attemptedWebhookCreate: {
|
||||
id: 'audit_table.attemptedWebhookCreate',
|
||||
defaultMessage: 'Attempted to create a webhook'
|
||||
},
|
||||
succcessfullWebhookCreate: {
|
||||
id: 'audit_table.successfullWebhookCreate',
|
||||
defaultMessage: 'Successfully created a webhook'
|
||||
},
|
||||
failedWebhookCreate: {
|
||||
id: 'audit_table.failedWebhookCreate',
|
||||
defaultMessage: 'Failed to create a webhook - bad channel permissions'
|
||||
},
|
||||
attemptedWebhookDelete: {
|
||||
id: 'audit_table.attemptedWebhookDelete',
|
||||
defaultMessage: 'Attempted to delete a webhook'
|
||||
},
|
||||
successfullWebhookDelete: {
|
||||
id: 'audit_table.successfullWebhookDelete',
|
||||
defaultMessage: 'Successfully deleted a webhook'
|
||||
},
|
||||
failedWebhookDelete: {
|
||||
id: 'audit_table.failedWebhookDelete',
|
||||
defaultMessage: 'Failed to delete a webhook - inappropriate conditions'
|
||||
},
|
||||
logout: {
|
||||
id: 'audit_table.logout',
|
||||
defaultMessage: 'Logged out of your account'
|
||||
},
|
||||
verified: {
|
||||
id: 'audit_table.verified',
|
||||
defaultMessage: 'Sucessfully verified your email address'
|
||||
},
|
||||
revokedAll: {
|
||||
id: 'audit_table.revokedAll',
|
||||
defaultMessage: 'Revoked all current sessions for the team'
|
||||
},
|
||||
loginAttempt: {
|
||||
id: 'audit_table.loginAttempt',
|
||||
defaultMessage: ' (Login attempt)'
|
||||
},
|
||||
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'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class AuditTable extends React.Component {
|
||||
render() {
|
||||
var accessList = [];
|
||||
|
||||
const {formatMessage} = this.props.intl;
|
||||
for (var i = 0; i < this.props.audits.length; i++) {
|
||||
const audit = this.props.audits[i];
|
||||
const auditInfo = formatAuditInfo(audit, formatMessage);
|
||||
|
||||
let uContent;
|
||||
if (this.props.showUserId) {
|
||||
uContent = <td>{auditInfo.userId}</td>;
|
||||
}
|
||||
|
||||
let iContent;
|
||||
if (this.props.showIp) {
|
||||
iContent = <td>{auditInfo.ip}</td>;
|
||||
}
|
||||
|
||||
let sContent;
|
||||
if (this.props.showSession) {
|
||||
sContent = <td>{auditInfo.sessionId}</td>;
|
||||
}
|
||||
|
||||
let descStyle = {};
|
||||
if (auditInfo.desc.toLowerCase().indexOf('fail') !== -1) {
|
||||
descStyle.color = 'red';
|
||||
}
|
||||
|
||||
accessList[i] = (
|
||||
<tr key={audit.id}>
|
||||
<td>{auditInfo.timestamp}</td>
|
||||
{uContent}
|
||||
<td style={descStyle}>{auditInfo.desc}</td>
|
||||
{iContent}
|
||||
{sContent}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
let userIdContent;
|
||||
if (this.props.showUserId) {
|
||||
userIdContent = (
|
||||
<th>
|
||||
<FormattedMessage
|
||||
id='audit_table.userId'
|
||||
defaultMessage='User ID'
|
||||
/>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
let ipContent;
|
||||
if (this.props.showIp) {
|
||||
ipContent = (
|
||||
<th>
|
||||
<FormattedMessage
|
||||
id='audit_table.ip'
|
||||
defaultMessage='IP Address'
|
||||
/>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
let sessionContent;
|
||||
if (this.props.showSession) {
|
||||
sessionContent = (
|
||||
<th>
|
||||
<FormattedMessage
|
||||
id='audit_table.session'
|
||||
defaultMessage='Session ID'
|
||||
/>
|
||||
</th>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<FormattedMessage
|
||||
id='audit_table.timestamp'
|
||||
defaultMessage='Timestamp'
|
||||
/>
|
||||
</th>
|
||||
{userIdContent}
|
||||
<th>
|
||||
<FormattedMessage
|
||||
id='audit_table.action'
|
||||
defaultMessage='Action'
|
||||
/>
|
||||
</th>
|
||||
{ipContent}
|
||||
{sessionContent}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{accessList}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuditTable.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
audits: React.PropTypes.array.isRequired,
|
||||
showUserId: React.PropTypes.bool,
|
||||
showIp: React.PropTypes.bool,
|
||||
showSession: React.PropTypes.bool
|
||||
};
|
||||
|
||||
export default injectIntl(AuditTable);
|
||||
|
||||
export function formatAuditInfo(audit, formatMessage) {
|
||||
const actionURL = audit.action.replace(/\/api\/v[1-9]/, '');
|
||||
let auditDesc = '';
|
||||
|
||||
if (actionURL.indexOf('/channels') === 0) {
|
||||
const channelInfo = audit.extra_info.split(' ');
|
||||
const channelNameField = channelInfo[0].split('=');
|
||||
|
||||
let channelURL = '';
|
||||
let channelObj;
|
||||
let channelName = '';
|
||||
if (channelNameField.indexOf('name') >= 0) {
|
||||
channelURL = channelNameField[channelNameField.indexOf('name') + 1];
|
||||
channelObj = ChannelStore.getByName(channelURL);
|
||||
if (channelObj) {
|
||||
channelName = channelObj.display_name;
|
||||
} else {
|
||||
channelName = channelURL;
|
||||
}
|
||||
}
|
||||
|
||||
switch (actionURL) {
|
||||
case '/channels/create':
|
||||
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});
|
||||
break;
|
||||
case '/channels/update_desc': // support the old path
|
||||
case '/channels/update_header':
|
||||
auditDesc = formatMessage(holders.headerUpdated, {channelName});
|
||||
break;
|
||||
default: {
|
||||
let userIdField = [];
|
||||
let userId = '';
|
||||
let username = '';
|
||||
|
||||
if (channelInfo[1]) {
|
||||
userIdField = channelInfo[1].split('=');
|
||||
|
||||
if (userIdField.indexOf('user_id') >= 0) {
|
||||
userId = userIdField[userIdField.indexOf('user_id') + 1];
|
||||
username = UserStore.getProfile(userId).username;
|
||||
}
|
||||
}
|
||||
|
||||
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, channelName});
|
||||
} else if (/\/channels\/[A-Za-z0-9]+\/remove/.test(actionURL)) {
|
||||
auditDesc = formatMessage(holders.userRemoved, {username, channelName});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (actionURL.indexOf('/oauth') === 0) {
|
||||
const oauthInfo = audit.extra_info.split(' ');
|
||||
|
||||
switch (actionURL) {
|
||||
case '/oauth/register': {
|
||||
const clientIdField = oauthInfo[0].split('=');
|
||||
|
||||
if (clientIdField[0] === 'client_id') {
|
||||
auditDesc = formatMessage(holders.attemptedRegisterApp, {id: clientIdField[1]});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case '/oauth/allow':
|
||||
if (oauthInfo[0] === 'attempt') {
|
||||
auditDesc = formatMessage(holders.attemptedAllowOAuthAccess);
|
||||
} else if (oauthInfo[0] === 'success') {
|
||||
auditDesc = formatMessage(holders.successfullOAuthAccess);
|
||||
} else if (oauthInfo[0] === 'fail - redirect_uri did not match registered callback') {
|
||||
auditDesc = formatMessage(holders.failedOAuthAccess);
|
||||
}
|
||||
|
||||
break;
|
||||
case '/oauth/access_token':
|
||||
if (oauthInfo[0] === 'attempt') {
|
||||
auditDesc = formatMessage(holders.attemptedOAuthToken);
|
||||
} else if (oauthInfo[0] === 'success') {
|
||||
auditDesc = formatMessage(holders.successfullOAuthToken);
|
||||
} else {
|
||||
const oauthTokenFailure = oauthInfo[0].split('-');
|
||||
|
||||
if (oauthTokenFailure[0].trim() === 'fail' && oauthTokenFailure[1]) {
|
||||
auditDesc = formatMessage(oauthTokenFailure, {token: oauthTokenFailure[1].trim()});
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (actionURL.indexOf('/users') === 0) {
|
||||
const userInfo = audit.extra_info.split(' ');
|
||||
|
||||
switch (actionURL) {
|
||||
case '/users/login':
|
||||
if (userInfo[0] === 'attempt') {
|
||||
auditDesc = formatMessage(holders.attemptedLogin);
|
||||
} else if (userInfo[0] === 'success') {
|
||||
auditDesc = formatMessage(holders.successfullLogin);
|
||||
} else if (userInfo[0]) {
|
||||
auditDesc = formatMessage(holders.failedLogin);
|
||||
}
|
||||
|
||||
break;
|
||||
case '/users/revoke_session':
|
||||
auditDesc = formatMessage(holders.sessionRevoked, {sessionId: userInfo[0].split('=')[1]});
|
||||
break;
|
||||
case '/users/newimage':
|
||||
auditDesc = formatMessage(holders.updatePicture);
|
||||
break;
|
||||
case '/users/update':
|
||||
auditDesc = formatMessage(holders.updateGeneral);
|
||||
break;
|
||||
case '/users/newpassword':
|
||||
if (userInfo[0] === 'attempted') {
|
||||
auditDesc = formatMessage(holders.attemptedPassword);
|
||||
} else if (userInfo[0] === 'completed') {
|
||||
auditDesc = formatMessage(holders.successfullPassword);
|
||||
} else if (userInfo[0] === 'failed - tried to update user password who was logged in through oauth') {
|
||||
auditDesc = formatMessage(holders.failedPassword);
|
||||
}
|
||||
|
||||
break;
|
||||
case '/users/update_roles': {
|
||||
const userRoles = userInfo[0].split('=')[1];
|
||||
|
||||
auditDesc = formatMessage(holders.updatedRol);
|
||||
if (userRoles.trim()) {
|
||||
auditDesc += userRoles;
|
||||
} else {
|
||||
auditDesc += formatMessage(holders.member);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case '/users/update_active': {
|
||||
const updateType = userInfo[0].split('=')[0];
|
||||
const updateField = userInfo[0].split('=')[1];
|
||||
|
||||
/* Either describes account activation/deactivation or a revoked session as part of an account deactivation */
|
||||
if (updateType === 'active') {
|
||||
if (updateField === 'true') {
|
||||
auditDesc = formatMessage(holders.accountActive);
|
||||
} else if (updateField === 'false') {
|
||||
auditDesc = formatMessage(holders.accountInactive);
|
||||
}
|
||||
|
||||
const actingUserInfo = userInfo[1].split('=');
|
||||
if (actingUserInfo[0] === 'session_user') {
|
||||
const actingUser = UserStore.getProfile(actingUserInfo[1]);
|
||||
const user = UserStore.getCurrentUser();
|
||||
if (user && actingUser && (Utils.isAdmin(user.roles) || Utils.isSystemAdmin(user.roles))) {
|
||||
auditDesc += formatMessage(holders.by, {username: actingUser.username});
|
||||
} else if (user && actingUser) {
|
||||
auditDesc += formatMessage(holders.byAdmin);
|
||||
}
|
||||
}
|
||||
} else if (updateType === 'session_id') {
|
||||
auditDesc = formatMessage(holders.sessionRevoked, {sessionId: updateField});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case '/users/send_password_reset':
|
||||
auditDesc = formatMessage(holders.sentEmail, {email: userInfo[0].split('=')[1]});
|
||||
break;
|
||||
case '/users/reset_password':
|
||||
if (userInfo[0] === 'attempt') {
|
||||
auditDesc = formatMessage(holders.attemptedReset);
|
||||
} else if (userInfo[0] === 'success') {
|
||||
auditDesc = formatMessage(holders.successfullReset);
|
||||
}
|
||||
|
||||
break;
|
||||
case '/users/update_notify':
|
||||
auditDesc = formatMessage(holders.updateGlobalNotifications);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (actionURL.indexOf('/hooks') === 0) {
|
||||
const webhookInfo = audit.extra_info;
|
||||
|
||||
switch (actionURL) {
|
||||
case '/hooks/incoming/create':
|
||||
if (webhookInfo === 'attempt') {
|
||||
auditDesc = formatMessage(holders.attemptedWebhookCreate);
|
||||
} else if (webhookInfo === 'success') {
|
||||
auditDesc = formatMessage(holders.succcessfullWebhookCreate);
|
||||
} else if (webhookInfo === 'fail - bad channel permissions') {
|
||||
auditDesc = formatMessage(holders.failedWebhookCreate);
|
||||
}
|
||||
|
||||
break;
|
||||
case '/hooks/incoming/delete':
|
||||
if (webhookInfo === 'attempt') {
|
||||
auditDesc = formatMessage(holders.attemptedWebhookDelete);
|
||||
} else if (webhookInfo === 'success') {
|
||||
auditDesc = formatMessage(holders.successfullWebhookDelete);
|
||||
} else if (webhookInfo === 'fail - inappropriate conditions') {
|
||||
auditDesc = formatMessage(holders.failedWebhookDelete);
|
||||
}
|
||||
|
||||
break;
|
||||
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':
|
||||
auditDesc = formatMessage(holders.logout);
|
||||
break;
|
||||
case '/verify_email':
|
||||
auditDesc = formatMessage(holders.verified);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* If all else fails... */
|
||||
if (!auditDesc) {
|
||||
/* Currently not called anywhere */
|
||||
if (audit.extra_info.indexOf('revoked_all=') >= 0) {
|
||||
auditDesc = formatMessage(holders.revokedAll);
|
||||
} else {
|
||||
let actionDesc = '';
|
||||
if (actionURL && actionURL.lastIndexOf('/') !== -1) {
|
||||
actionDesc = actionURL.substring(actionURL.lastIndexOf('/') + 1).replace('_', ' ');
|
||||
actionDesc = Utils.toTitleCase(actionDesc);
|
||||
}
|
||||
|
||||
let extraInfoDesc = '';
|
||||
if (audit.extra_info) {
|
||||
extraInfoDesc = audit.extra_info;
|
||||
|
||||
if (extraInfoDesc.indexOf('=') !== -1) {
|
||||
extraInfoDesc = extraInfoDesc.substring(extraInfoDesc.indexOf('=') + 1);
|
||||
}
|
||||
}
|
||||
auditDesc = actionDesc + ' ' + extraInfoDesc;
|
||||
}
|
||||
}
|
||||
|
||||
const date = new Date(audit.create_at);
|
||||
const auditInfo = {};
|
||||
auditInfo.timestamp = (
|
||||
<div>
|
||||
<FormattedDate
|
||||
value={date}
|
||||
day='2-digit'
|
||||
month='short'
|
||||
year='numeric'
|
||||
/>
|
||||
{' - '}
|
||||
<FormattedTime
|
||||
value={date}
|
||||
hour='2-digit'
|
||||
minute='2-digit'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
auditInfo.userId = audit.user_id;
|
||||
auditInfo.desc = auditDesc;
|
||||
auditInfo.ip = audit.ip_address;
|
||||
auditInfo.sessionId = audit.session_id;
|
||||
|
||||
return auditInfo;
|
||||
}
|
||||
119
webapp/components/authorize.jsx
Обычный файл
119
webapp/components/authorize.jsx
Обычный файл
@@ -0,0 +1,119 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Client from 'utils/client.jsx';
|
||||
|
||||
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import icon50 from 'images/icon50x50.png';
|
||||
|
||||
export default class Authorize extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleAllow = this.handleAllow.bind(this);
|
||||
this.handleDeny = this.handleDeny.bind(this);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
handleAllow() {
|
||||
const responseType = this.props.responseType;
|
||||
const clientId = this.props.clientId;
|
||||
const redirectUri = this.props.redirectUri;
|
||||
const state = this.props.state;
|
||||
const scope = this.props.scope;
|
||||
|
||||
Client.allowOAuth2(responseType, clientId, redirectUri, state, scope,
|
||||
(data) => {
|
||||
if (data.redirect) {
|
||||
window.location.replace(data.redirect);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
//Do nothing on error
|
||||
}
|
||||
);
|
||||
}
|
||||
handleDeny() {
|
||||
window.location.replace(this.props.redirectUri + '?error=access_denied');
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className='container-fluid'>
|
||||
<div className='prompt'>
|
||||
<div className='prompt__heading'>
|
||||
<div className='prompt__app-icon'>
|
||||
<img
|
||||
src={icon50}
|
||||
width='50'
|
||||
height='50'
|
||||
alt=''
|
||||
/>
|
||||
</div>
|
||||
<div className='text'>
|
||||
<FormattedMessage
|
||||
id='authorize.title'
|
||||
defaultMessage='An application would like to connect to your {teamName} account'
|
||||
values={{
|
||||
teamName: this.props.teamName
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p>
|
||||
<FormattedHTMLMessage
|
||||
id='authorize.app'
|
||||
defaultMessage='The app <strong>{appName}</strong> would like the ability to access and modify your basic information.'
|
||||
values={{
|
||||
appName: this.props.appName
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<h2 className='prompt__allow'>
|
||||
<FormattedHTMLMessage
|
||||
id='authorize.access'
|
||||
defaultMessage='Allow <strong>{appName}</strong> access?'
|
||||
values={{
|
||||
appName: this.props.appName
|
||||
}}
|
||||
/>
|
||||
</h2>
|
||||
<div className='prompt__buttons'>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn authorize-btn'
|
||||
onClick={this.handleDeny}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='authorize.deny'
|
||||
defaultMessage='Deny'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary authorize-btn'
|
||||
onClick={this.handleAllow}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='authorize.allow'
|
||||
defaultMessage='Allow'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Authorize.propTypes = {
|
||||
appName: React.PropTypes.string,
|
||||
teamName: React.PropTypes.string,
|
||||
responseType: React.PropTypes.string,
|
||||
clientId: React.PropTypes.string,
|
||||
redirectUri: React.PropTypes.string,
|
||||
state: React.PropTypes.string,
|
||||
scope: React.PropTypes.string
|
||||
};
|
||||
141
webapp/components/center_panel.jsx
Обычный файл
141
webapp/components/center_panel.jsx
Обычный файл
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import TutorialIntroScreens from './tutorial/tutorial_intro_screens.jsx';
|
||||
import CreatePost from './create_post.jsx';
|
||||
import PostsViewContainer from './posts_view_container.jsx';
|
||||
import PostFocusView from './post_focus_view.jsx';
|
||||
import ChannelHeader from './channel_header.jsx';
|
||||
import Navbar from './navbar.jsx';
|
||||
import FileUploadOverlay from './file_upload_overlay.jsx';
|
||||
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const TutorialSteps = Constants.TutorialSteps;
|
||||
const Preferences = Constants.Preferences;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class CenterPanel extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||
this.validState = this.validState.bind(this);
|
||||
this.onStoresChange = this.onStoresChange.bind(this);
|
||||
|
||||
this.state = this.getStateFromStores();
|
||||
}
|
||||
getStateFromStores() {
|
||||
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
|
||||
return {
|
||||
showTutorialScreens: tutorialStep <= TutorialSteps.INTRO_SCREENS,
|
||||
showPostFocus: ChannelStore.getPostMode() === ChannelStore.POST_MODE_FOCUS,
|
||||
user: UserStore.getCurrentUser(),
|
||||
channel: ChannelStore.getCurrent(),
|
||||
profiles: JSON.parse(JSON.stringify(UserStore.getProfiles()))
|
||||
};
|
||||
}
|
||||
validState() {
|
||||
return this.state.user && this.state.channel && this.state.profiles;
|
||||
}
|
||||
onStoresChange() {
|
||||
this.setState(this.getStateFromStores());
|
||||
}
|
||||
componentDidMount() {
|
||||
PreferenceStore.addChangeListener(this.onStoresChange);
|
||||
ChannelStore.addChangeListener(this.onStoresChange);
|
||||
UserStore.addChangeListener(this.onStoresChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
PreferenceStore.removeChangeListener(this.onStoresChange);
|
||||
ChannelStore.removeChangeListener(this.onStoresChange);
|
||||
UserStore.removeChangeListener(this.onStoresChange);
|
||||
}
|
||||
render() {
|
||||
if (!this.validState()) {
|
||||
return null;
|
||||
}
|
||||
const channel = this.state.channel;
|
||||
var handleClick = null;
|
||||
let postsContainer;
|
||||
let createPost;
|
||||
if (this.state.showTutorialScreens) {
|
||||
postsContainer = <TutorialIntroScreens/>;
|
||||
createPost = null;
|
||||
} else if (this.state.showPostFocus) {
|
||||
postsContainer = <PostFocusView profiles={this.state.profiles}/>;
|
||||
|
||||
handleClick = function clickHandler(e) {
|
||||
e.preventDefault();
|
||||
Utils.switchChannel(channel);
|
||||
};
|
||||
|
||||
createPost = (
|
||||
<div
|
||||
id='archive-link-home'
|
||||
onClick={handleClick}
|
||||
>
|
||||
<a href=''>
|
||||
<FormattedMessage
|
||||
id='center_panel.recent'
|
||||
defaultMessage='Click here to jump to recent messages. '
|
||||
/>
|
||||
<i className='fa fa-arrow-down'></i>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
postsContainer = <PostsViewContainer profiles={this.state.profiles}/>;
|
||||
createPost = (
|
||||
<div
|
||||
className='post-create__container'
|
||||
id='post-create'
|
||||
>
|
||||
<CreatePost/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='inner-wrap channel__wrap'>
|
||||
<div className='row header'>
|
||||
<div id='navbar'>
|
||||
<Navbar/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='row main'>
|
||||
<FileUploadOverlay
|
||||
id='file_upload_overlay'
|
||||
overlayType='center'
|
||||
/>
|
||||
<div
|
||||
id='app-content'
|
||||
className='app__content'
|
||||
>
|
||||
<div id='channel-header'>
|
||||
<ChannelHeader
|
||||
user={this.state.user}
|
||||
/>
|
||||
</div>
|
||||
{postsContainer}
|
||||
{createPost}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CenterPanel.defaultProps = {
|
||||
};
|
||||
|
||||
CenterPanel.propTypes = {
|
||||
};
|
||||
222
webapp/components/change_url_modal.jsx
Обычный файл
222
webapp/components/change_url_modal.jsx
Обычный файл
@@ -0,0 +1,222 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class ChangeUrlModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onURLChanged = this.onURLChanged.bind(this);
|
||||
this.doSubmit = this.doSubmit.bind(this);
|
||||
this.doCancel = this.doCancel.bind(this);
|
||||
|
||||
this.state = {
|
||||
currentURL: props.currentURL,
|
||||
urlError: '',
|
||||
userEdit: false
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// This check prevents the url being deleted when we re-render
|
||||
// because of user status check
|
||||
if (!this.state.userEdit) {
|
||||
this.setState({
|
||||
currentURL: nextProps.currentURL
|
||||
});
|
||||
}
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.show === true && prevProps.show === false) {
|
||||
ReactDOM.findDOMNode(this.refs.urlinput).select();
|
||||
}
|
||||
}
|
||||
onURLChanged(e) {
|
||||
const url = e.target.value.trim();
|
||||
this.setState({currentURL: url.replace(/[^A-Za-z0-9-_]/g, '').toLowerCase(), userEdit: true});
|
||||
}
|
||||
getURLError(url) {
|
||||
let error = []; //eslint-disable-line prefer-const
|
||||
if (url.length < 2) {
|
||||
error.push(
|
||||
<span key='error1'>
|
||||
<FormattedMessage
|
||||
id='change_url.longer'
|
||||
defaultMessage='Must be longer than two characters'
|
||||
/>
|
||||
<br/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (url.charAt(0) === '-' || url.charAt(0) === '_') {
|
||||
error.push(
|
||||
<span key='error2'>
|
||||
<FormattedMessage
|
||||
id='change_url.startWithLetter'
|
||||
defaultMessage='Must start with a letter or number'
|
||||
/>
|
||||
<br/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (url.length > 1 && (url.charAt(url.length - 1) === '-' || url.charAt(url.length - 1) === '_')) {
|
||||
error.push(
|
||||
<span key='error3'>
|
||||
<FormattedMessage
|
||||
id='change_url.endWithLetter'
|
||||
defaultMessage='Must end with a letter or number'
|
||||
/>
|
||||
<br/>
|
||||
</span>);
|
||||
}
|
||||
if (url.indexOf('__') > -1) {
|
||||
error.push(
|
||||
<span key='error4'>
|
||||
<FormattedMessage
|
||||
id='change_url.noUnderscore'
|
||||
defaultMessage='Can not contain two underscores in a row.'
|
||||
/>
|
||||
<br/>
|
||||
</span>);
|
||||
}
|
||||
|
||||
// In case of error we don't detect
|
||||
if (error.length === 0) {
|
||||
error.push(
|
||||
<span key='errorlast'>
|
||||
<FormattedMessage
|
||||
id='change_url.invalidUrl'
|
||||
defaultMessage='Invalid URL'
|
||||
/>
|
||||
<br/>
|
||||
</span>);
|
||||
}
|
||||
return error;
|
||||
}
|
||||
doSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const url = ReactDOM.findDOMNode(this.refs.urlinput).value;
|
||||
const cleanedURL = Utils.cleanUpUrlable(url);
|
||||
if (cleanedURL !== url || url.length < 2 || url.indexOf('__') > -1) {
|
||||
this.setState({urlError: this.getURLError(url)});
|
||||
return;
|
||||
}
|
||||
this.setState({urlError: '', userEdit: false});
|
||||
this.props.onModalSubmit(url);
|
||||
}
|
||||
doCancel() {
|
||||
this.setState({urlError: '', userEdit: false});
|
||||
this.props.onModalDismissed();
|
||||
}
|
||||
render() {
|
||||
let urlClass = 'input-group input-group--limit';
|
||||
let urlError = null;
|
||||
let serverError = null;
|
||||
|
||||
if (this.state.urlError) {
|
||||
urlClass += ' has-error';
|
||||
urlError = (<p className='input__help error'>{this.state.urlError}</p>);
|
||||
}
|
||||
|
||||
if (this.props.serverError) {
|
||||
serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>;
|
||||
}
|
||||
|
||||
const fullTeamUrl = Utils.getTeamURLFromAddressBar();
|
||||
const teamURL = Utils.getShortenedTeamURL();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.doCancel}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>{this.props.title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<form
|
||||
role='form'
|
||||
className='form-horizontal'
|
||||
>
|
||||
<Modal.Body>
|
||||
<div className='modal-intro'>{this.props.description}</div>
|
||||
<div className='form-group'>
|
||||
<label className='col-sm-2 form__label control-label'>{this.props.urlLabel}</label>
|
||||
<div className='col-sm-10'>
|
||||
<div className={urlClass}>
|
||||
<span
|
||||
data-toggle='tooltip'
|
||||
title={fullTeamUrl}
|
||||
className='input-group-addon'
|
||||
>
|
||||
{teamURL}
|
||||
</span>
|
||||
<input
|
||||
type='text'
|
||||
ref='urlinput'
|
||||
className='form-control'
|
||||
maxLength='22'
|
||||
onChange={this.onURLChanged}
|
||||
value={this.state.currentURL}
|
||||
autoFocus={true}
|
||||
tabIndex='1'
|
||||
/>
|
||||
</div>
|
||||
{urlError}
|
||||
{serverError}
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.doCancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='change_url.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={this.doSubmit}
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
tabIndex='2'
|
||||
>
|
||||
{this.props.submitButtonText}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChangeUrlModal.defaultProps = {
|
||||
show: false,
|
||||
title: 'Change URL',
|
||||
desciption: '',
|
||||
urlLabel: 'URL',
|
||||
submitButtonText: 'Save',
|
||||
currentURL: '',
|
||||
serverError: ''
|
||||
};
|
||||
|
||||
ChangeUrlModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
title: React.PropTypes.string,
|
||||
description: React.PropTypes.string,
|
||||
urlLabel: React.PropTypes.string,
|
||||
submitButtonText: React.PropTypes.string,
|
||||
currentURL: React.PropTypes.string,
|
||||
serverError: React.PropTypes.string,
|
||||
onModalSubmit: React.PropTypes.func.isRequired,
|
||||
onModalDismissed: React.PropTypes.func.isRequired
|
||||
};
|
||||
520
webapp/components/channel_header.jsx
Обычный файл
520
webapp/components/channel_header.jsx
Обычный файл
@@ -0,0 +1,520 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import 'bootstrap';
|
||||
import NavbarSearchBox from './search_bar.jsx';
|
||||
import MessageWrapper from './message_wrapper.jsx';
|
||||
import PopoverListMembers from './popover_list_members.jsx';
|
||||
import EditChannelHeaderModal from './edit_channel_header_modal.jsx';
|
||||
import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx';
|
||||
import ChannelInfoModal from './channel_info_modal.jsx';
|
||||
import ChannelInviteModal from './channel_invite_modal.jsx';
|
||||
import ChannelMembersModal from './channel_members_modal.jsx';
|
||||
import ChannelNotificationsModal from './channel_notifications_modal.jsx';
|
||||
import DeleteChannelModal from './delete_channel_modal.jsx';
|
||||
import RenameChannelModal from './rename_channel_modal.jsx';
|
||||
import ToggleModalButton from './toggle_modal_button.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import SearchStore from 'stores/search_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
|
||||
import {Tooltip, OverlayTrigger, Popover} from 'react-bootstrap';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class ChannelHeader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
this.handleLeave = this.handleLeave.bind(this);
|
||||
this.searchMentions = this.searchMentions.bind(this);
|
||||
this.showRenameChannelModal = this.showRenameChannelModal.bind(this);
|
||||
this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this);
|
||||
|
||||
const state = this.getStateFromStores();
|
||||
state.showEditChannelPurposeModal = false;
|
||||
state.showMembersModal = false;
|
||||
state.showRenameChannelModal = false;
|
||||
this.state = state;
|
||||
}
|
||||
getStateFromStores() {
|
||||
const extraInfo = ChannelStore.getCurrentExtraInfo();
|
||||
|
||||
return {
|
||||
channel: ChannelStore.getCurrent(),
|
||||
memberChannel: ChannelStore.getCurrentMember(),
|
||||
users: extraInfo.members,
|
||||
userCount: extraInfo.member_count,
|
||||
searchVisible: SearchStore.getSearchResults() !== null,
|
||||
currentUser: UserStore.getCurrentUser()
|
||||
};
|
||||
}
|
||||
validState() {
|
||||
if (!this.state.channel ||
|
||||
!this.state.memberChannel ||
|
||||
!this.state.users ||
|
||||
!this.state.userCount ||
|
||||
!this.state.currentUser) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onListenerChange);
|
||||
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
|
||||
SearchStore.addSearchChangeListener(this.onListenerChange);
|
||||
PreferenceStore.addChangeListener(this.onListenerChange);
|
||||
UserStore.addChangeListener(this.onListenerChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
||||
SearchStore.removeSearchChangeListener(this.onListenerChange);
|
||||
PreferenceStore.removeChangeListener(this.onListenerChange);
|
||||
UserStore.removeChangeListener(this.onListenerChange);
|
||||
}
|
||||
onListenerChange() {
|
||||
const newState = this.getStateFromStores();
|
||||
if (!Utils.areObjectsEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
$('.channel-header__info .description').popover({placement: 'bottom', trigger: 'hover', html: true, delay: {show: 500, hide: 500}});
|
||||
}
|
||||
handleLeave() {
|
||||
Client.leaveChannel(this.state.channel.id,
|
||||
() => {
|
||||
AppDispatcher.handleViewAction({
|
||||
type: ActionTypes.LEAVE_CHANNEL,
|
||||
id: this.state.channel.id
|
||||
});
|
||||
|
||||
const townsquare = ChannelStore.getByName('town-square');
|
||||
Utils.switchChannel(townsquare);
|
||||
},
|
||||
(err) => {
|
||||
AsyncClient.dispatchError(err, 'handleLeave');
|
||||
}
|
||||
);
|
||||
}
|
||||
searchMentions(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const user = this.state.currentUser;
|
||||
|
||||
let terms = '';
|
||||
if (user.notify_props && user.notify_props.mention_keys) {
|
||||
const termKeys = UserStore.getMentionKeys(user.id);
|
||||
|
||||
if (user.notify_props.all === 'true' && termKeys.indexOf('@all') !== -1) {
|
||||
termKeys.splice(termKeys.indexOf('@all'), 1);
|
||||
}
|
||||
|
||||
if (user.notify_props.channel === 'true' && termKeys.indexOf('@channel') !== -1) {
|
||||
termKeys.splice(termKeys.indexOf('@channel'), 1);
|
||||
}
|
||||
terms = termKeys.join(' ');
|
||||
}
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_SEARCH_TERM,
|
||||
term: terms,
|
||||
do_search: true,
|
||||
is_mention_search: true
|
||||
});
|
||||
}
|
||||
showRenameChannelModal(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({
|
||||
showRenameChannelModal: true
|
||||
});
|
||||
}
|
||||
hideRenameChannelModal() {
|
||||
this.setState({
|
||||
showRenameChannelModal: false
|
||||
});
|
||||
}
|
||||
render() {
|
||||
if (!this.validState()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const channel = this.state.channel;
|
||||
const recentMentionsTooltip = (
|
||||
<Tooltip id='recentMentionsTooltip'>
|
||||
<FormattedMessage
|
||||
id='channel_header.recentMentions'
|
||||
defaultMessage='Recent Mentions'
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
const popoverContent = (
|
||||
<Popover
|
||||
id='hader-popover'
|
||||
bStyle='info'
|
||||
bSize='large'
|
||||
placement='bottom'
|
||||
className='description'
|
||||
onMouseOver={() => this.refs.headerOverlay.show()}
|
||||
onMouseOut={() => this.refs.headerOverlay.hide()}
|
||||
>
|
||||
<MessageWrapper
|
||||
message={channel.header}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
let channelTitle = channel.display_name;
|
||||
const currentId = this.state.currentUser.id;
|
||||
const isAdmin = Utils.isAdmin(this.state.memberChannel.roles) || Utils.isAdmin(this.state.currentUser.roles);
|
||||
const isDirect = (this.state.channel.type === 'D');
|
||||
|
||||
if (isDirect) {
|
||||
if (this.state.users.length > 1) {
|
||||
let contact;
|
||||
if (this.state.users[0].id === currentId) {
|
||||
contact = this.state.users[1];
|
||||
} else {
|
||||
contact = this.state.users[0];
|
||||
}
|
||||
channelTitle = Utils.displayUsername(contact.id);
|
||||
}
|
||||
}
|
||||
|
||||
let channelTerm = (
|
||||
<FormattedMessage
|
||||
id='channel_header.channel'
|
||||
defaultMessage='Channel'
|
||||
/>
|
||||
);
|
||||
if (channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
channelTerm = (
|
||||
<FormattedMessage
|
||||
id='channel_header.group'
|
||||
defaultMessage='Group'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let popoverListMembers;
|
||||
if (!isDirect) {
|
||||
popoverListMembers = (
|
||||
<PopoverListMembers
|
||||
members={this.state.users}
|
||||
memberCount={this.state.userCount}
|
||||
channelId={channel.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const dropdownContents = [];
|
||||
if (isDirect) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='edit_header_direct'
|
||||
role='presentation'
|
||||
>
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={EditChannelHeaderModal}
|
||||
dialogProps={{channel}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.channelHeader'
|
||||
defaultMessage='Set Channel Header...'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</li>
|
||||
);
|
||||
} else {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='view_info'
|
||||
role='presentation'
|
||||
>
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={ChannelInfoModal}
|
||||
dialogProps={{channel}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.viewInfo'
|
||||
defaultMessage='View Info'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</li>
|
||||
);
|
||||
|
||||
if (!ChannelStore.isDefault(channel)) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='add_members'
|
||||
role='presentation'
|
||||
>
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={ChannelInviteModal}
|
||||
dialogProps={{channel, currentUser: this.state.currentUser}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='chanel_header.addMembers'
|
||||
defaultMessage='Add Members'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</li>
|
||||
);
|
||||
|
||||
if (isAdmin) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='manage_members'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={() => this.setState({showMembersModal: true})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.manageMembers'
|
||||
defaultMessage='Manage Members'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='set_channel_header'
|
||||
role='presentation'
|
||||
>
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={EditChannelHeaderModal}
|
||||
dialogProps={{channel}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.setHeader'
|
||||
defaultMessage='Set {term} Header...'
|
||||
values={{
|
||||
term: (channelTerm)
|
||||
}}
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</li>
|
||||
);
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='set_channel_purpose'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={() => this.setState({showEditChannelPurposeModal: true})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.setPurpose'
|
||||
defaultMessage='Set {term} Purpose...'
|
||||
values={{
|
||||
term: (channelTerm)
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='notification_preferences'
|
||||
role='presentation'
|
||||
>
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={ChannelNotificationsModal}
|
||||
dialogProps={{
|
||||
channel,
|
||||
channelMember: this.state.memberChannel,
|
||||
currentUser: this.state.currentUser
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.notificationPreferences'
|
||||
defaultMessage='Notification Preferences'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</li>
|
||||
);
|
||||
|
||||
if (isAdmin) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='rename_channel'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.showRenameChannelModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.rename'
|
||||
defaultMessage='Rename {term}...'
|
||||
values={{
|
||||
term: (channelTerm)
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
||||
if (!ChannelStore.isDefault(channel)) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='delete_channel'
|
||||
role='presentation'
|
||||
>
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={DeleteChannelModal}
|
||||
dialogProps={{channel}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.delete'
|
||||
defaultMessage='Delete {term}...'
|
||||
values={{
|
||||
term: (channelTerm)
|
||||
}}
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!ChannelStore.isDefault(channel)) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='leave_channel'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.handleLeave}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_header.leave'
|
||||
defaultMessage='Leave {term}'
|
||||
values={{
|
||||
term: (channelTerm)
|
||||
}}
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table className='channel-header alt'>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
<div className='channel-header__info'>
|
||||
<div className='dropdown'>
|
||||
<a
|
||||
href='#'
|
||||
className='dropdown-toggle theme'
|
||||
type='button'
|
||||
id='channel_header_dropdown'
|
||||
data-toggle='dropdown'
|
||||
aria-expanded='true'
|
||||
>
|
||||
<strong className='heading'>{channelTitle} </strong>
|
||||
<span className='glyphicon glyphicon-chevron-down header-dropdown__icon'/>
|
||||
</a>
|
||||
<ul
|
||||
className='dropdown-menu'
|
||||
role='menu'
|
||||
aria-labelledby='channel_header_dropdown'
|
||||
>
|
||||
{dropdownContents}
|
||||
</ul>
|
||||
</div>
|
||||
<OverlayTrigger
|
||||
trigger={'click'}
|
||||
placement='bottom'
|
||||
overlay={popoverContent}
|
||||
ref='headerOverlay'
|
||||
>
|
||||
<div
|
||||
onClick={TextFormatting.handleClick}
|
||||
className='description'
|
||||
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
{popoverListMembers}
|
||||
</th>
|
||||
<th className='search-bar__container'><NavbarSearchBox/></th>
|
||||
<th>
|
||||
<div className='dropdown channel-header__links'>
|
||||
<OverlayTrigger
|
||||
delayShow={Constants.OVERLAY_TIME_DELAY}
|
||||
placement='bottom'
|
||||
overlay={recentMentionsTooltip}
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
type='button'
|
||||
onClick={this.searchMentions}
|
||||
>
|
||||
{'@'}
|
||||
</a>
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<EditChannelPurposeModal
|
||||
show={this.state.showEditChannelPurposeModal}
|
||||
onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
|
||||
channel={channel}
|
||||
/>
|
||||
<ChannelMembersModal
|
||||
show={this.state.showMembersModal}
|
||||
onModalDismissed={() => this.setState({showMembersModal: false})}
|
||||
channel={channel}
|
||||
/>
|
||||
<RenameChannelModal
|
||||
show={this.state.showRenameChannelModal}
|
||||
onHide={this.hideRenameChannelModal}
|
||||
channel={channel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChannelHeader.propTypes = {
|
||||
};
|
||||
104
webapp/components/channel_info_modal.jsx
Обычный файл
104
webapp/components/channel_info_modal.jsx
Обычный файл
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
const holders = defineMessages({
|
||||
notFound: {
|
||||
id: 'channel_info.notFound',
|
||||
defaultMessage: 'No Channel Found'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class ChannelInfoModal extends React.Component {
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
let channel = this.props.channel;
|
||||
if (!channel) {
|
||||
channel = {
|
||||
display_name: formatMessage(holders.notFound),
|
||||
name: formatMessage(holders.notFound),
|
||||
purpose: formatMessage(holders.notFound),
|
||||
id: formatMessage(holders.notFound)
|
||||
};
|
||||
}
|
||||
|
||||
const channelURL = Utils.getShortenedTeamURL() + channel.name;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.props.onHide}
|
||||
>
|
||||
<Modal.Header closeButtton={true}>
|
||||
{channel.display_name}
|
||||
</Modal.Header>
|
||||
<Modal.Body ref='modalBody'>
|
||||
<div className='row form-group'>
|
||||
<div className='col-sm-3 info__label'>
|
||||
<FormattedMessage
|
||||
id='channel_info.name'
|
||||
defaultMessage='Channel Name:'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-9'>{channel.display_name}</div>
|
||||
</div>
|
||||
<div className='row form-group'>
|
||||
<div className='col-sm-3 info__label'>
|
||||
<FormattedMessage
|
||||
id='channel_info.url'
|
||||
defaultMessage='Channel URL:'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-9'>{channelURL}</div>
|
||||
</div>
|
||||
<div className='row form-group'>
|
||||
<div className='col-sm-3 info__label'>
|
||||
<FormattedMessage
|
||||
id='channel_info.id'
|
||||
defaultMessage='Channel ID:'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-9'>{channel.id}</div>
|
||||
</div>
|
||||
<div className='row'>
|
||||
<div className='col-sm-3 info__label'>
|
||||
<FormattedMessage
|
||||
id='channel_info.purpose'
|
||||
defaultMessage='Channel Purpose:'
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-9'>{channel.purpose}</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.props.onHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_info.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChannelInfoModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired,
|
||||
channel: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(ChannelInfoModal);
|
||||
217
webapp/components/channel_invite_modal.jsx
Обычный файл
217
webapp/components/channel_invite_modal.jsx
Обычный файл
@@ -0,0 +1,217 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import FilteredUserList from './filtered_user_list.jsx';
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class ChannelInviteModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
this.handleInvite = this.handleInvite.bind(this);
|
||||
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||
this.createInviteButton = this.createInviteButton.bind(this);
|
||||
|
||||
this.state = this.getStateFromStores();
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!this.props.show && !nextProps.show) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(this.props, nextProps)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(this.state, nextState)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
getStateFromStores() {
|
||||
const users = UserStore.getActiveOnlyProfiles();
|
||||
|
||||
if ($.isEmptyObject(users)) {
|
||||
return {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
// make sure we have all members of this channel before rendering
|
||||
const extraInfo = ChannelStore.getCurrentExtraInfo();
|
||||
if (extraInfo.member_count !== extraInfo.members.length) {
|
||||
AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
|
||||
|
||||
return {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
const currentUser = UserStore.getCurrentUser();
|
||||
if (!currentUser) {
|
||||
return {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
const currentMember = ChannelStore.getCurrentMember();
|
||||
if (!currentMember) {
|
||||
return {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
const memberIds = extraInfo.members.map((user) => user.id);
|
||||
|
||||
var nonmembers = [];
|
||||
for (var id in users) {
|
||||
if (memberIds.indexOf(id) === -1) {
|
||||
nonmembers.push(users[id]);
|
||||
}
|
||||
}
|
||||
|
||||
nonmembers.sort((a, b) => {
|
||||
return a.username.localeCompare(b.username);
|
||||
});
|
||||
|
||||
return {
|
||||
nonmembers,
|
||||
loading: false,
|
||||
currentUser,
|
||||
currentMember
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.show && nextProps.show) {
|
||||
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
|
||||
ChannelStore.addChangeListener(this.onListenerChange);
|
||||
UserStore.addChangeListener(this.onListenerChange);
|
||||
this.onListenerChange();
|
||||
} else if (this.props.show && !nextProps.show) {
|
||||
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||
UserStore.removeChangeListener(this.onListenerChange);
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||
UserStore.removeChangeListener(this.onListenerChange);
|
||||
}
|
||||
onListenerChange() {
|
||||
var newState = this.getStateFromStores();
|
||||
if (!Utils.areObjectsEqual(this.state, newState)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
handleInvite(user) {
|
||||
const data = {
|
||||
user_id: user.id
|
||||
};
|
||||
|
||||
Client.addChannelMember(
|
||||
this.props.channel.id,
|
||||
data,
|
||||
() => {
|
||||
this.setState({inviteError: null});
|
||||
AsyncClient.getChannelExtraInfo();
|
||||
},
|
||||
(err) => {
|
||||
this.setState({inviteError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
createInviteButton({user}) {
|
||||
return (
|
||||
<a
|
||||
onClick={this.handleInvite.bind(this, user)}
|
||||
className='btn btn-sm btn-primary'
|
||||
>
|
||||
<i className='glyphicon glyphicon-envelope'/>
|
||||
<FormattedMessage
|
||||
id='channel_invite.add'
|
||||
defaultMessage=' Add'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
var inviteError = null;
|
||||
if (this.state.inviteError) {
|
||||
inviteError = (<label className='has-error control-label'>{this.state.inviteError}</label>);
|
||||
}
|
||||
|
||||
var content;
|
||||
if (this.state.loading) {
|
||||
content = (<LoadingScreen/>);
|
||||
} else {
|
||||
let maxHeight = 1000;
|
||||
if (Utils.windowHeight() <= 1200) {
|
||||
maxHeight = Utils.windowHeight() - 300;
|
||||
}
|
||||
content = (
|
||||
<FilteredUserList
|
||||
style={{maxHeight}}
|
||||
users={this.state.nonmembers}
|
||||
actions={[this.createInviteButton]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='more-modal'
|
||||
show={this.props.show}
|
||||
onHide={this.props.onHide}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='channel_invite.addNewMembers'
|
||||
defaultMessage='Add New Members to '
|
||||
/>
|
||||
<span className='name'>{this.props.channel.display_name}</span>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{inviteError}
|
||||
{content}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.props.onHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_invite.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChannelInviteModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired,
|
||||
channel: React.PropTypes.object.isRequired
|
||||
};
|
||||
225
webapp/components/channel_members_modal.jsx
Обычный файл
225
webapp/components/channel_members_modal.jsx
Обычный файл
@@ -0,0 +1,225 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import FilteredUserList from './filtered_user_list.jsx';
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
import ChannelInviteModal from './channel_invite_modal.jsx';
|
||||
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class ChannelMembersModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.handleRemove = this.handleRemove.bind(this);
|
||||
|
||||
this.createRemoveMemberButton = this.createRemoveMemberButton.bind(this);
|
||||
|
||||
// the rest of the state gets populated when the modal is shown
|
||||
this.state = {
|
||||
showInviteModal: false
|
||||
};
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(this.props, nextProps)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(this.state, nextState)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
getStateFromStores() {
|
||||
const extraInfo = ChannelStore.getCurrentExtraInfo();
|
||||
const profiles = UserStore.getActiveOnlyProfiles();
|
||||
|
||||
if (extraInfo.member_count !== extraInfo.members.length) {
|
||||
AsyncClient.getChannelExtraInfo(this.props.channel.id, -1);
|
||||
|
||||
return {
|
||||
loading: true
|
||||
};
|
||||
}
|
||||
|
||||
const memberList = extraInfo.members.map((member) => {
|
||||
return profiles[member.id];
|
||||
});
|
||||
|
||||
function compareByUsername(a, b) {
|
||||
if (a.username < b.username) {
|
||||
return -1;
|
||||
} else if (a.username > b.username) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
memberList.sort(compareByUsername);
|
||||
|
||||
return {
|
||||
memberList,
|
||||
loading: false
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.show && nextProps.show) {
|
||||
ChannelStore.addExtraInfoChangeListener(this.onChange);
|
||||
ChannelStore.addChangeListener(this.onChange);
|
||||
|
||||
this.onChange();
|
||||
} else if (this.props.show && !nextProps.show) {
|
||||
ChannelStore.removeExtraInfoChangeListener(this.onChange);
|
||||
ChannelStore.removeChangeListener(this.onChange);
|
||||
}
|
||||
}
|
||||
onChange() {
|
||||
const newState = this.getStateFromStores();
|
||||
if (!Utils.areObjectsEqual(this.state, newState)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
handleRemove(user) {
|
||||
const userId = user.id;
|
||||
|
||||
const data = {};
|
||||
data.user_id = userId;
|
||||
|
||||
Client.removeChannelMember(
|
||||
ChannelStore.getCurrentId(),
|
||||
data,
|
||||
() => {
|
||||
const memberList = this.state.memberList.slice();
|
||||
for (let i = 0; i < memberList.length; i++) {
|
||||
if (userId === memberList[i].id) {
|
||||
memberList.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({memberList});
|
||||
AsyncClient.getChannelExtraInfo();
|
||||
},
|
||||
(err) => {
|
||||
this.setState({inviteError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
createRemoveMemberButton({user}) {
|
||||
if (user.id === UserStore.getCurrentId()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary btn-message'
|
||||
onClick={this.handleRemove.bind(this, user)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_members_modal.remove'
|
||||
defaultMessage='Remove'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
let content;
|
||||
if (this.state.loading) {
|
||||
content = (<LoadingScreen/>);
|
||||
} else {
|
||||
let maxHeight = 1000;
|
||||
if (Utils.windowHeight() <= 1200) {
|
||||
maxHeight = Utils.windowHeight() - 300;
|
||||
}
|
||||
|
||||
content = (
|
||||
<FilteredUserList
|
||||
style={{maxHeight}}
|
||||
users={this.state.memberList}
|
||||
actions={[this.createRemoveMemberButton]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
dialogClassName='more-modal'
|
||||
show={this.props.show}
|
||||
onHide={this.props.onModalDismissed}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<span className='name'>{this.props.channel.display_name}</span>
|
||||
<FormattedMessage
|
||||
id='channel_memebers_modal.members'
|
||||
defaultMessage=' Members'
|
||||
/>
|
||||
</Modal.Title>
|
||||
<a
|
||||
className='btn btn-md btn-primary'
|
||||
href='#'
|
||||
onClick={() => {
|
||||
this.setState({showInviteModal: true});
|
||||
this.props.onModalDismissed();
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_members_modal.addNew'
|
||||
defaultMessage=' Add New Members'
|
||||
/>
|
||||
</a>
|
||||
</Modal.Header>
|
||||
<Modal.Body
|
||||
ref='modalBody'
|
||||
>
|
||||
{content}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.props.onModalDismissed}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_members_modal.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
<ChannelInviteModal
|
||||
show={this.state.showInviteModal}
|
||||
onHide={() => this.setState({showInviteModal: false})}
|
||||
channel={this.props.channel}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChannelMembersModal.defaultProps = {
|
||||
show: false
|
||||
};
|
||||
|
||||
ChannelMembersModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onModalDismissed: React.PropTypes.func.isRequired,
|
||||
channel: React.PropTypes.object.isRequired
|
||||
};
|
||||
417
webapp/components/channel_notifications_modal.jsx
Обычный файл
417
webapp/components/channel_notifications_modal.jsx
Обычный файл
@@ -0,0 +1,417 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import SettingItemMin from './setting_item_min.jsx';
|
||||
import SettingItemMax from './setting_item_max.jsx';
|
||||
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class ChannelNotificationsModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.updateSection = this.updateSection.bind(this);
|
||||
|
||||
this.handleSubmitNotifyLevel = this.handleSubmitNotifyLevel.bind(this);
|
||||
this.handleUpdateNotifyLevel = this.handleUpdateNotifyLevel.bind(this);
|
||||
this.createNotifyLevelSection = this.createNotifyLevelSection.bind(this);
|
||||
|
||||
this.handleSubmitMarkUnreadLevel = this.handleSubmitMarkUnreadLevel.bind(this);
|
||||
this.handleUpdateMarkUnreadLevel = this.handleUpdateMarkUnreadLevel.bind(this);
|
||||
this.createMarkUnreadLevelSection = this.createMarkUnreadLevelSection.bind(this);
|
||||
|
||||
this.state = {
|
||||
activeSection: '',
|
||||
notifyLevel: '',
|
||||
unreadLevel: ''
|
||||
};
|
||||
}
|
||||
updateSection(section) {
|
||||
this.setState({activeSection: section});
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.show && nextProps.show) {
|
||||
this.setState({
|
||||
notifyLevel: nextProps.channelMember.notify_props.desktop,
|
||||
unreadLevel: nextProps.channelMember.notify_props.mark_unread
|
||||
});
|
||||
}
|
||||
}
|
||||
handleSubmitNotifyLevel() {
|
||||
var channelId = this.props.channel.id;
|
||||
var notifyLevel = this.state.notifyLevel;
|
||||
|
||||
if (this.props.channelMember.notify_props.desktop === notifyLevel) {
|
||||
this.updateSection('');
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {};
|
||||
data.channel_id = channelId;
|
||||
data.user_id = this.props.currentUser.id;
|
||||
data.desktop = notifyLevel;
|
||||
|
||||
//TODO: This should be moved to event_helpers
|
||||
Client.updateNotifyProps(data,
|
||||
() => {
|
||||
// YUCK
|
||||
var member = ChannelStore.getMember(channelId);
|
||||
member.notify_props.desktop = notifyLevel;
|
||||
ChannelStore.setChannelMember(member);
|
||||
this.updateSection('');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
handleUpdateNotifyLevel(notifyLevel) {
|
||||
this.setState({notifyLevel});
|
||||
}
|
||||
createNotifyLevelSection(serverError) {
|
||||
// Get glabal user setting for notifications
|
||||
const globalNotifyLevel = this.props.currentUser.notify_props.desktop;
|
||||
let globalNotifyLevelName;
|
||||
if (globalNotifyLevel === 'all') {
|
||||
globalNotifyLevelName = (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.allActivity'
|
||||
defaultMessage='For all activity'
|
||||
/>
|
||||
);
|
||||
} else if (globalNotifyLevel === 'mention') {
|
||||
globalNotifyLevelName = (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.onlyMentions'
|
||||
defaultMessage='Only for mentions'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
globalNotifyLevelName = (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.never'
|
||||
defaultMessage='Never'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const sendDesktop = (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.sendDesktop'
|
||||
defaultMessage='Send desktop notifications'
|
||||
/>
|
||||
);
|
||||
|
||||
const notificationLevel = this.state.notifyLevel;
|
||||
|
||||
if (this.state.activeSection === 'desktop') {
|
||||
const notifyActive = [false, false, false, false];
|
||||
if (notificationLevel === 'default') {
|
||||
notifyActive[0] = true;
|
||||
} else if (notificationLevel === 'all') {
|
||||
notifyActive[1] = true;
|
||||
} else if (notificationLevel === 'mention') {
|
||||
notifyActive[2] = true;
|
||||
} else {
|
||||
notifyActive[3] = true;
|
||||
}
|
||||
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div key='channel-notification-level-radio'>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
type='radio'
|
||||
checked={notifyActive[0]}
|
||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='channel_notifications.globalDefault'
|
||||
defaultMessage='Global default ({notifyLevel})'
|
||||
values={{
|
||||
notifyLevel: (globalNotifyLevelName)
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
type='radio'
|
||||
checked={notifyActive[1]}
|
||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
|
||||
/>
|
||||
<FormattedMessage id='channel_notifications.allActivity'/>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
type='radio'
|
||||
checked={notifyActive[2]}
|
||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
|
||||
/>
|
||||
<FormattedMessage id='channel_notifications.onlyMentions'/>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
type='radio'
|
||||
checked={notifyActive[3]}
|
||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
|
||||
/>
|
||||
<FormattedMessage id='channel_notifications.never'/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const handleUpdateSection = function updateSection(e) {
|
||||
this.updateSection('');
|
||||
this.onListenerChange();
|
||||
e.preventDefault();
|
||||
}.bind(this);
|
||||
|
||||
const extraInfo = (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='channel_notifications.override'
|
||||
defaultMessage='Selecting an option other than "Default" will override the global notification settings. Desktop notifications are available on Firefox, Safari, and Chrome.'
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
return (
|
||||
<SettingItemMax
|
||||
title={sendDesktop}
|
||||
inputs={inputs}
|
||||
submit={this.handleSubmitNotifyLevel}
|
||||
server_error={serverError}
|
||||
updateSection={handleUpdateSection}
|
||||
extraInfo={extraInfo}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
var describe;
|
||||
if (notificationLevel === 'default') {
|
||||
describe = (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.globalDefault'
|
||||
values={{
|
||||
notifyLevel: (globalNotifyLevelName)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (notificationLevel === 'mention') {
|
||||
describe = (<FormattedMessage id='channel_notifications.onlyMentions'/>);
|
||||
} else if (notificationLevel === 'all') {
|
||||
describe = (<FormattedMessage id='channel_notifications.allActivity'/>);
|
||||
} else {
|
||||
describe = (<FormattedMessage id='channel_notifications.never'/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingItemMin
|
||||
title={sendDesktop}
|
||||
describe={describe}
|
||||
updateSection={() => {
|
||||
this.updateSection('desktop');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
handleSubmitMarkUnreadLevel() {
|
||||
const channelId = this.props.channel.id;
|
||||
const markUnreadLevel = this.state.unreadLevel;
|
||||
|
||||
if (this.props.channelMember.notify_props.mark_unread === markUnreadLevel) {
|
||||
this.updateSection('');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
channel_id: channelId,
|
||||
user_id: this.props.currentUser.id,
|
||||
mark_unread: markUnreadLevel
|
||||
};
|
||||
|
||||
//TODO: This should be fixed, moved to event_helpers
|
||||
Client.updateNotifyProps(data,
|
||||
() => {
|
||||
// Yuck...
|
||||
var member = ChannelStore.getMember(channelId);
|
||||
member.notify_props.mark_unread = markUnreadLevel;
|
||||
ChannelStore.setChannelMember(member);
|
||||
this.updateSection('');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleUpdateMarkUnreadLevel(unreadLevel) {
|
||||
this.setState({unreadLevel});
|
||||
}
|
||||
|
||||
createMarkUnreadLevelSection(serverError) {
|
||||
let content;
|
||||
|
||||
const markUnread = (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.markUnread'
|
||||
defaultMessage='Mark Channel Unread'
|
||||
/>
|
||||
);
|
||||
if (this.state.activeSection === 'markUnreadLevel') {
|
||||
const inputs = [(
|
||||
<div key='channel-notification-unread-radio'>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
type='radio'
|
||||
checked={this.state.unreadLevel === 'all'}
|
||||
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='channel_notifications.allUnread'
|
||||
defaultMessage='For all unread messages'
|
||||
/>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
type='radio'
|
||||
checked={this.state.unreadLevel === 'mention'}
|
||||
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
|
||||
/>
|
||||
<FormattedMessage id='channel_notifications.onlyMentions'/>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
)];
|
||||
|
||||
const handleUpdateSection = function handleUpdateSection(e) {
|
||||
this.updateSection('');
|
||||
this.onListenerChange();
|
||||
e.preventDefault();
|
||||
}.bind(this);
|
||||
|
||||
const extraInfo = (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='channel_notifications.unreadInfo'
|
||||
defaultMessage='The channel name is bolded in the sidebar when there are unread messages. Selecting "Only for mentions" will bold the channel only when you are mentioned.'
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
|
||||
content = (
|
||||
<SettingItemMax
|
||||
title={markUnread}
|
||||
inputs={inputs}
|
||||
submit={this.handleSubmitMarkUnreadLevel}
|
||||
server_error={serverError}
|
||||
updateSection={handleUpdateSection}
|
||||
extraInfo={extraInfo}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
let describe;
|
||||
|
||||
if (!this.state.unreadLevel || this.state.unreadLevel === 'all') {
|
||||
describe = (
|
||||
<FormattedMessage
|
||||
id='channel_notifications.allUnread'
|
||||
defaultMessage='For all unread messages'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
describe = (<FormattedMessage id='channel_notifications.onlyMentions'/>);
|
||||
}
|
||||
|
||||
const handleUpdateSection = function handleUpdateSection(e) {
|
||||
this.updateSection('markUnreadLevel');
|
||||
e.preventDefault();
|
||||
}.bind(this);
|
||||
|
||||
content = (
|
||||
<SettingItemMin
|
||||
title={markUnread}
|
||||
describe={describe}
|
||||
updateSection={handleUpdateSection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
render() {
|
||||
var serverError = null;
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
dialogClassName='settings-modal'
|
||||
onHide={this.props.onHide}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='channel_notifications.preferences'
|
||||
defaultMessage='Notification Preferences for '
|
||||
/>
|
||||
<span className='name'>{this.props.channel.display_name}</span>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<div className='settings-table'>
|
||||
<div className='settings-content'>
|
||||
<div
|
||||
ref='wrapper'
|
||||
className='user-settings'
|
||||
>
|
||||
<br/>
|
||||
<div className='divider-dark first'/>
|
||||
{this.createNotifyLevelSection(serverError)}
|
||||
<div className='divider-light'/>
|
||||
{this.createMarkUnreadLevelSection(serverError)}
|
||||
<div className='divider-dark'/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{serverError}
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChannelNotificationsModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired,
|
||||
channel: React.PropTypes.object.isRequired,
|
||||
channelMember: React.PropTypes.object.isRequired,
|
||||
currentUser: React.PropTypes.object.isRequired
|
||||
};
|
||||
20
webapp/components/channel_view.jsx
Обычный файл
20
webapp/components/channel_view.jsx
Обычный файл
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import CenterPanel from 'components/center_panel.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class ChannelView extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<CenterPanel/>
|
||||
);
|
||||
}
|
||||
}
|
||||
ChannelView.defaultProps = {
|
||||
};
|
||||
|
||||
ChannelView.propTypes = {
|
||||
params: React.PropTypes.object
|
||||
};
|
||||
116
webapp/components/claim/claim_account.jsx
Обычный файл
116
webapp/components/claim/claim_account.jsx
Обычный файл
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import EmailToSSO from './email_to_sso.jsx';
|
||||
import SSOToEmail from './sso_to_email.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
import logoImage from 'images/logo.png';
|
||||
|
||||
export default class ClaimAccount extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onTeamChange = this.onTeamChange.bind(this);
|
||||
this.updateStateFromStores = this.updateStateFromStores.bind(this);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
email: this.props.location.query.email,
|
||||
newType: this.props.location.query.new_type,
|
||||
oldType: this.props.location.query.old_type,
|
||||
teamName: this.props.params.team,
|
||||
teamDisplayName: ''
|
||||
});
|
||||
this.updateStateFromStores();
|
||||
}
|
||||
componentDidMount() {
|
||||
TeamStore.addChangeListener(this.onTeamChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
TeamStore.removeChangeListener(this.onTeamChange);
|
||||
}
|
||||
updateStateFromStores() {
|
||||
const team = TeamStore.getByName(this.state.teamName);
|
||||
let displayName = '';
|
||||
if (team) {
|
||||
displayName = team.displayName;
|
||||
}
|
||||
this.setState({
|
||||
teamDisplayName: displayName
|
||||
});
|
||||
}
|
||||
onTeamChange() {
|
||||
this.updateStateFromStores();
|
||||
}
|
||||
render() {
|
||||
if (this.state.teamDisplayName === '') {
|
||||
return (<div/>);
|
||||
}
|
||||
let content;
|
||||
if (this.state.email === '') {
|
||||
content = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='claim.account.noEmail'
|
||||
defaultMessage='No email specified'
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
} else if (this.state.oldType === '' && this.state.newType !== '') {
|
||||
content = (
|
||||
<EmailToSSO
|
||||
email={this.state.email}
|
||||
type={this.state.newType}
|
||||
teamName={this.state.teamName}
|
||||
teamDisplayName={this.state.teamDisplayName}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<SSOToEmail
|
||||
email={this.state.email}
|
||||
currentType={this.state.oldType}
|
||||
teamName={this.state.teamName}
|
||||
teamDisplayName={this.state.teamDisplayName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='signup-header'>
|
||||
<a href='/'>
|
||||
<span className='fa fa-chevron-left'/>
|
||||
<FormattedMessage
|
||||
id='web.header.back'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className='col-sm-12'>
|
||||
<div className='signup-team__container'>
|
||||
<img
|
||||
className='signup-team-logo'
|
||||
src={logoImage}
|
||||
/>
|
||||
<div id='claim'>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ClaimAccount.defaultProps = {
|
||||
};
|
||||
ClaimAccount.propTypes = {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
location: React.PropTypes.object.isRequired
|
||||
};
|
||||
154
webapp/components/claim/email_to_sso.jsx
Обычный файл
154
webapp/components/claim/email_to_sso.jsx
Обычный файл
@@ -0,0 +1,154 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
pwdError: {
|
||||
id: 'claim.email_to_sso.pwdError',
|
||||
defaultMessage: 'Please enter your password.'
|
||||
},
|
||||
pwd: {
|
||||
id: 'claim.email_to_sso.pwd',
|
||||
defaultMessage: 'Password'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class EmailToSSO extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.submit = this.submit.bind(this);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
submit(e) {
|
||||
e.preventDefault();
|
||||
var state = {};
|
||||
|
||||
var password = ReactDOM.findDOMNode(this.refs.password).value.trim();
|
||||
if (!password) {
|
||||
state.error = this.props.intl.formatMessage(holders.pwdError);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.error = null;
|
||||
this.setState(state);
|
||||
|
||||
var postData = {};
|
||||
postData.password = password;
|
||||
postData.email = this.props.email;
|
||||
postData.team_name = this.props.teamName;
|
||||
postData.service = this.props.type;
|
||||
|
||||
Client.switchToSSO(postData,
|
||||
(data) => {
|
||||
if (data.follow_link) {
|
||||
window.location.href = data.follow_link;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
this.setState({error});
|
||||
}
|
||||
);
|
||||
}
|
||||
render() {
|
||||
var error = null;
|
||||
if (this.state.error) {
|
||||
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
|
||||
}
|
||||
|
||||
var formClass = 'form-group';
|
||||
if (error) {
|
||||
formClass += ' has-error';
|
||||
}
|
||||
|
||||
const uiType = Utils.toTitleCase(this.props.type) + ' SSO';
|
||||
|
||||
return (
|
||||
<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.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.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
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EmailToSSO.defaultProps = {
|
||||
};
|
||||
EmailToSSO.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
type: React.PropTypes.string.isRequired,
|
||||
email: React.PropTypes.string.isRequired,
|
||||
teamName: React.PropTypes.string.isRequired,
|
||||
teamDisplayName: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(EmailToSSO);
|
||||
168
webapp/components/claim/sso_to_email.jsx
Обычный файл
168
webapp/components/claim/sso_to_email.jsx
Обычный файл
@@ -0,0 +1,168 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
enterPwd: {
|
||||
id: 'claim.sso_to_email.enterPwd',
|
||||
defaultMessage: 'Please enter a password.'
|
||||
},
|
||||
pwdNotMatch: {
|
||||
id: 'claim.sso_to_email.pwdNotMatch',
|
||||
defaultMessage: 'Password do not match.'
|
||||
},
|
||||
newPwd: {
|
||||
id: 'claim.sso_to_email.newPwd',
|
||||
defaultMessage: 'New Password'
|
||||
},
|
||||
confirm: {
|
||||
id: 'claim.sso_to_email.confirm',
|
||||
defaultMessage: 'Confirm Password'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class SSOToEmail extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.submit = this.submit.bind(this);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
submit(e) {
|
||||
const {formatMessage} = this.props.intl;
|
||||
e.preventDefault();
|
||||
const state = {};
|
||||
|
||||
const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
|
||||
if (!password) {
|
||||
state.error = formatMessage(holders.enterPwd);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const confirmPassword = ReactDOM.findDOMNode(this.refs.passwordconfirm).value.trim();
|
||||
if (!confirmPassword || password !== confirmPassword) {
|
||||
state.error = formatMessage(holders.pwdNotMatch);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.error = null;
|
||||
this.setState(state);
|
||||
|
||||
var postData = {};
|
||||
postData.password = password;
|
||||
postData.email = this.props.email;
|
||||
postData.team_name = this.props.teamName;
|
||||
|
||||
Client.switchToEmail(postData,
|
||||
(data) => {
|
||||
if (data.follow_link) {
|
||||
window.location.href = data.follow_link;
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
this.setState({error});
|
||||
}
|
||||
);
|
||||
}
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
var error = null;
|
||||
if (this.state.error) {
|
||||
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
|
||||
}
|
||||
|
||||
var formClass = 'form-group';
|
||||
if (error) {
|
||||
formClass += ' has-error';
|
||||
}
|
||||
|
||||
const uiType = Utils.toTitleCase(this.props.currentType) + ' SSO';
|
||||
|
||||
return (
|
||||
<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.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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SSOToEmail.defaultProps = {
|
||||
};
|
||||
SSOToEmail.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
currentType: React.PropTypes.string.isRequired,
|
||||
email: React.PropTypes.string.isRequired,
|
||||
teamName: React.PropTypes.string.isRequired,
|
||||
teamDisplayName: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default injectIntl(SSOToEmail);
|
||||
69
webapp/components/confirm_modal.jsx
Обычный файл
69
webapp/components/confirm_modal.jsx
Обычный файл
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class ConfirmModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleConfirm = this.handleConfirm.bind(this);
|
||||
}
|
||||
|
||||
handleConfirm() {
|
||||
this.props.onConfirm();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Modal
|
||||
className='modal-confirm'
|
||||
show={this.props.show}
|
||||
onHide={this.props.onCancel}
|
||||
>
|
||||
<Modal.Header closeButton={false}>
|
||||
<Modal.Title>{this.props.title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{this.props.message}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.props.onCancel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='confirm_modal.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
onClick={this.props.onConfirm}
|
||||
>
|
||||
{this.props.confirmButton}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ConfirmModal.defaultProps = {
|
||||
title: '',
|
||||
message: '',
|
||||
confirmButton: ''
|
||||
};
|
||||
ConfirmModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
title: React.PropTypes.node,
|
||||
message: React.PropTypes.node,
|
||||
confirmButton: React.PropTypes.node,
|
||||
onConfirm: React.PropTypes.func.isRequired,
|
||||
onCancel: React.PropTypes.func.isRequired
|
||||
};
|
||||
448
webapp/components/create_comment.jsx
Обычный файл
448
webapp/components/create_comment.jsx
Обычный файл
@@ -0,0 +1,448 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
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';
|
||||
import MsgTyping from './msg_typing.jsx';
|
||||
import FileUpload from './file_upload.jsx';
|
||||
import FilePreview from './file_preview.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
const KeyCodes = Constants.KeyCodes;
|
||||
|
||||
const holders = defineMessages({
|
||||
commentLength: {
|
||||
id: 'create_comment.commentLength',
|
||||
defaultMessage: 'Comment length must be less than {max} characters.'
|
||||
},
|
||||
comment: {
|
||||
id: 'create_comment.comment',
|
||||
defaultMessage: 'Add Comment'
|
||||
},
|
||||
addComment: {
|
||||
id: 'create_comment.addComment',
|
||||
defaultMessage: 'Add a comment...'
|
||||
},
|
||||
commentTitle: {
|
||||
id: 'create_comment.commentTitle',
|
||||
defaultMessage: 'Comment'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class CreateComment extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.lastTime = 0;
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.commentMsgKeyPress = this.commentMsgKeyPress.bind(this);
|
||||
this.handleUserInput = this.handleUserInput.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleUploadClick = this.handleUploadClick.bind(this);
|
||||
this.handleUploadStart = this.handleUploadStart.bind(this);
|
||||
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
|
||||
this.handleUploadError = this.handleUploadError.bind(this);
|
||||
this.removePreview = this.removePreview.bind(this);
|
||||
this.getFileCount = this.getFileCount.bind(this);
|
||||
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();
|
||||
|
||||
const draft = PostStore.getCommentDraft(this.props.rootId);
|
||||
this.state = {
|
||||
messageText: draft.message,
|
||||
uploadsInProgress: draft.uploadsInProgress,
|
||||
previews: draft.previews,
|
||||
submitting: false,
|
||||
windowWidth: Utils.windowWidth(),
|
||||
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
|
||||
showPostDeletedModal: false
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
|
||||
this.focusTextbox();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
onPreferenceChange() {
|
||||
this.setState({
|
||||
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter')
|
||||
});
|
||||
}
|
||||
handleResize() {
|
||||
this.setState({windowWidth: Utils.windowWidth()});
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.uploadsInProgress < this.state.uploadsInProgress) {
|
||||
$('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
|
||||
}
|
||||
|
||||
if (prevProps.rootId !== this.props.rootId) {
|
||||
this.focusTextbox();
|
||||
}
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.uploadsInProgress.length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
let post = {};
|
||||
post.filenames = [];
|
||||
post.message = this.state.messageText;
|
||||
|
||||
if (post.message.trim().length === 0 && this.state.previews.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (post.message.length > Constants.CHARACTER_LIMIT) {
|
||||
this.setState({postError: this.props.intl.formatMessage(holders.commentLength, {max: Constants.CHARACTER_LIMIT})});
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = UserStore.getCurrentId();
|
||||
|
||||
post.channel_id = this.props.channelId;
|
||||
post.root_id = this.props.rootId;
|
||||
post.parent_id = this.props.rootId;
|
||||
post.filenames = this.state.previews;
|
||||
const time = Utils.getTimestamp();
|
||||
post.pending_post_id = `${userId}:${time}`;
|
||||
post.user_id = userId;
|
||||
post.create_at = time;
|
||||
|
||||
PostStore.storePendingPost(post);
|
||||
PostStore.storeCommentDraft(this.props.rootId, null);
|
||||
|
||||
Client.createPost(
|
||||
post,
|
||||
ChannelStore.getCurrent(),
|
||||
(data) => {
|
||||
AsyncClient.getPosts(this.props.channelId);
|
||||
|
||||
const channel = ChannelStore.get(this.props.channelId);
|
||||
let member = ChannelStore.getMember(this.props.channelId);
|
||||
member.msg_count = channel.total_msg_count;
|
||||
member.last_viewed_at = Date.now();
|
||||
ChannelStore.setChannelMember(member);
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_POST,
|
||||
post: data
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
if (err.id === 'api.post.create_post.root_id.app_error') {
|
||||
this.showPostDeletedModal();
|
||||
|
||||
PostStore.removePendingPost(post.channel_id, post.pending_post_id);
|
||||
} else {
|
||||
post.state = Constants.POST_FAILED;
|
||||
PostStore.updatePendingPost(post);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
submitting: false
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
this.setState({
|
||||
messageText: '',
|
||||
submitting: false,
|
||||
postError: null,
|
||||
previews: [],
|
||||
serverError: null
|
||||
});
|
||||
}
|
||||
commentMsgKeyPress(e) {
|
||||
if (this.state.ctrlSend && e.ctrlKey || !this.state.ctrlSend) {
|
||||
if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
ReactDOM.findDOMNode(this.refs.textbox).blur();
|
||||
this.handleSubmit(e);
|
||||
}
|
||||
}
|
||||
|
||||
const t = Date.now();
|
||||
if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
|
||||
SocketStore.sendMessage({channel_id: this.props.channelId, action: 'typing', props: {parent_id: this.props.rootId}});
|
||||
this.lastTime = t;
|
||||
}
|
||||
}
|
||||
handleUserInput(messageText) {
|
||||
let draft = PostStore.getCommentDraft(this.props.rootId);
|
||||
draft.message = messageText;
|
||||
PostStore.storeCommentDraft(this.props.rootId, draft);
|
||||
|
||||
$('.post-right__scroll').scrollTop($('.post-right__scroll')[0].scrollHeight);
|
||||
this.setState({messageText: messageText});
|
||||
}
|
||||
handleKeyDown(e) {
|
||||
if (this.state.ctrlSend && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
|
||||
this.commentMsgKeyPress(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
|
||||
e.preventDefault();
|
||||
|
||||
const lastPost = PostStore.getCurrentUsersLatestPost(this.props.channelId, this.props.rootId);
|
||||
if (!lastPost) {
|
||||
return;
|
||||
}
|
||||
|
||||
AppDispatcher.handleViewAction({
|
||||
type: ActionTypes.RECEIVED_EDIT_POST,
|
||||
refocusId: '#reply_textbox',
|
||||
title: this.props.intl.formatMessage(holders.commentTitle),
|
||||
message: lastPost.message,
|
||||
postId: lastPost.id,
|
||||
channelId: lastPost.channel_id,
|
||||
comments: PostStore.getCommentCount(lastPost)
|
||||
});
|
||||
}
|
||||
}
|
||||
handleUploadClick() {
|
||||
this.focusTextbox();
|
||||
}
|
||||
handleUploadStart(clientIds) {
|
||||
let draft = PostStore.getCommentDraft(this.props.rootId);
|
||||
|
||||
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
|
||||
PostStore.storeCommentDraft(this.props.rootId, draft);
|
||||
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress});
|
||||
|
||||
// this is a bit redundant with the code that sets focus when the file input is clicked,
|
||||
// but this also resets the focus after a drag and drop
|
||||
this.focusTextbox();
|
||||
}
|
||||
handleFileUploadComplete(filenames, clientIds) {
|
||||
let draft = PostStore.getCommentDraft(this.props.rootId);
|
||||
|
||||
// remove each finished file from uploads
|
||||
for (let i = 0; i < clientIds.length; i++) {
|
||||
const index = draft.uploadsInProgress.indexOf(clientIds[i]);
|
||||
|
||||
if (index !== -1) {
|
||||
draft.uploadsInProgress.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
draft.previews = draft.previews.concat(filenames);
|
||||
PostStore.storeCommentDraft(this.props.rootId, draft);
|
||||
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
|
||||
}
|
||||
handleUploadError(err, clientId) {
|
||||
if (clientId === -1) {
|
||||
this.setState({serverError: err});
|
||||
} else {
|
||||
let draft = PostStore.getCommentDraft(this.props.rootId);
|
||||
|
||||
const index = draft.uploadsInProgress.indexOf(clientId);
|
||||
if (index !== -1) {
|
||||
draft.uploadsInProgress.splice(index, 1);
|
||||
}
|
||||
|
||||
PostStore.storeCommentDraft(this.props.rootId, draft);
|
||||
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err});
|
||||
}
|
||||
}
|
||||
removePreview(id) {
|
||||
let previews = this.state.previews;
|
||||
let uploadsInProgress = this.state.uploadsInProgress;
|
||||
|
||||
// id can either be the path of an uploaded file or the client id of an in progress upload
|
||||
let index = previews.indexOf(id);
|
||||
if (index === -1) {
|
||||
index = uploadsInProgress.indexOf(id);
|
||||
|
||||
if (index !== -1) {
|
||||
uploadsInProgress.splice(index, 1);
|
||||
this.refs.fileUpload.getWrappedInstance().cancelUpload(id);
|
||||
}
|
||||
} else {
|
||||
previews.splice(index, 1);
|
||||
}
|
||||
|
||||
let draft = PostStore.getCommentDraft(this.props.rootId);
|
||||
draft.previews = previews;
|
||||
draft.uploadsInProgress = uploadsInProgress;
|
||||
PostStore.storeCommentDraft(this.props.rootId, draft);
|
||||
|
||||
this.setState({previews: previews, uploadsInProgress: uploadsInProgress});
|
||||
}
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.rootId !== this.props.rootId) {
|
||||
const draft = PostStore.getCommentDraft(newProps.rootId);
|
||||
this.setState({messageText: draft.message, uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
|
||||
}
|
||||
}
|
||||
getFileCount() {
|
||||
return this.state.previews.length + this.state.uploadsInProgress.length;
|
||||
}
|
||||
focusTextbox() {
|
||||
if (!Utils.isMobile()) {
|
||||
this.refs.textbox.focus();
|
||||
}
|
||||
}
|
||||
showPostDeletedModal() {
|
||||
this.setState({
|
||||
showPostDeletedModal: true
|
||||
});
|
||||
}
|
||||
hidePostDeletedModal() {
|
||||
this.setState({
|
||||
showPostDeletedModal: false
|
||||
});
|
||||
}
|
||||
render() {
|
||||
let serverError = null;
|
||||
if (this.state.serverError) {
|
||||
serverError = (
|
||||
<div className='form-group has-error'>
|
||||
<label className='control-label'>{this.state.serverError}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let postError = null;
|
||||
if (this.state.postError) {
|
||||
postError = <label className='control-label'>{this.state.postError}</label>;
|
||||
}
|
||||
|
||||
let preview = null;
|
||||
if (this.state.previews.length > 0 || this.state.uploadsInProgress.length > 0) {
|
||||
preview = (
|
||||
<FilePreview
|
||||
files={this.state.previews}
|
||||
onRemove={this.removePreview}
|
||||
uploadsInProgress={this.state.uploadsInProgress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let postFooterClassName = 'post-create-footer';
|
||||
if (postError) {
|
||||
postFooterClassName += ' has-error';
|
||||
}
|
||||
|
||||
let uploadsInProgressText = null;
|
||||
if (this.state.uploadsInProgress.length > 0) {
|
||||
uploadsInProgressText = (
|
||||
<span className='pull-right post-right-comments-upload-in-progress'>
|
||||
{this.state.uploadsInProgress.length === 1 ? (
|
||||
<FormattedMessage
|
||||
id='create_comment.file'
|
||||
defaultMessage='File uploading'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='create_comment.files'
|
||||
defaultMessage='Files uploading'
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const {formatMessage} = this.props.intl;
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className='post-create'>
|
||||
<div
|
||||
id={this.props.rootId}
|
||||
className='post-create-body comment-create-body'
|
||||
>
|
||||
<div className='post-body__cell'>
|
||||
<Textbox
|
||||
onUserInput={this.handleUserInput}
|
||||
onKeyPress={this.commentMsgKeyPress}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
messageText={this.state.messageText}
|
||||
createMessage={formatMessage(holders.addComment)}
|
||||
initialText=''
|
||||
supportsCommands={false}
|
||||
id='reply_textbox'
|
||||
ref='textbox'
|
||||
/>
|
||||
<FileUpload
|
||||
ref='fileUpload'
|
||||
getFileCount={this.getFileCount}
|
||||
onClick={this.handleUploadClick}
|
||||
onUploadStart={this.handleUploadStart}
|
||||
onFileUpload={this.handleFileUploadComplete}
|
||||
onUploadError={this.handleUploadError}
|
||||
postType='comment'
|
||||
channelId={this.props.channelId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<MsgTyping
|
||||
channelId={this.props.channelId}
|
||||
parentId={this.props.rootId}
|
||||
/>
|
||||
<div className={postFooterClassName}>
|
||||
<input
|
||||
type='button'
|
||||
className='btn btn-primary comment-btn pull-right'
|
||||
value={formatMessage(holders.comment)}
|
||||
onClick={this.handleSubmit}
|
||||
/>
|
||||
{uploadsInProgressText}
|
||||
{preview}
|
||||
{postError}
|
||||
{serverError}
|
||||
</div>
|
||||
</div>
|
||||
<PostDeletedModal
|
||||
show={this.state.showPostDeletedModal}
|
||||
onHide={this.hidePostDeletedModal}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CreateComment.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
channelId: React.PropTypes.string.isRequired,
|
||||
rootId: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(CreateComment);
|
||||
510
webapp/components/create_post.jsx
Обычный файл
510
webapp/components/create_post.jsx
Обычный файл
@@ -0,0 +1,510 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
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';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import SocketStore from 'stores/socket_store.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedHTMLMessage} from 'react-intl';
|
||||
|
||||
const Preferences = Constants.Preferences;
|
||||
const TutorialSteps = Constants.TutorialSteps;
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
const KeyCodes = Constants.KeyCodes;
|
||||
|
||||
const holders = defineMessages({
|
||||
comment: {
|
||||
id: 'create_post.comment',
|
||||
defaultMessage: 'Comment'
|
||||
},
|
||||
post: {
|
||||
id: 'create_post.post',
|
||||
defaultMessage: 'Post'
|
||||
},
|
||||
write: {
|
||||
id: 'create_post.write',
|
||||
defaultMessage: 'Write a message...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class CreatePost extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.lastTime = 0;
|
||||
|
||||
this.getCurrentDraft = this.getCurrentDraft.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.postMsgKeyPress = this.postMsgKeyPress.bind(this);
|
||||
this.handleUserInput = this.handleUserInput.bind(this);
|
||||
this.handleUploadClick = this.handleUploadClick.bind(this);
|
||||
this.handleUploadStart = this.handleUploadStart.bind(this);
|
||||
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
|
||||
this.handleUploadError = this.handleUploadError.bind(this);
|
||||
this.removePreview = this.removePreview.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
this.getFileCount = this.getFileCount.bind(this);
|
||||
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();
|
||||
|
||||
const draft = this.getCurrentDraft();
|
||||
|
||||
this.state = {
|
||||
channelId: ChannelStore.getCurrentId(),
|
||||
messageText: draft.messageText,
|
||||
uploadsInProgress: draft.uploadsInProgress,
|
||||
previews: draft.previews,
|
||||
submitting: false,
|
||||
initialText: draft.messageText,
|
||||
ctrlSend: false,
|
||||
showTutorialTip: false,
|
||||
showPostDeletedModal: false
|
||||
};
|
||||
}
|
||||
getCurrentDraft() {
|
||||
const draft = PostStore.getCurrentDraft();
|
||||
const safeDraft = {previews: [], messageText: '', uploadsInProgress: []};
|
||||
|
||||
if (draft) {
|
||||
if (draft.message) {
|
||||
safeDraft.messageText = draft.message;
|
||||
}
|
||||
if (draft.previews) {
|
||||
safeDraft.previews = draft.previews;
|
||||
}
|
||||
if (draft.uploadsInProgress) {
|
||||
safeDraft.uploadsInProgress = draft.uploadsInProgress;
|
||||
}
|
||||
}
|
||||
|
||||
return safeDraft;
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.uploadsInProgress.length > 0 || this.state.submitting) {
|
||||
return;
|
||||
}
|
||||
|
||||
const post = {};
|
||||
post.filenames = [];
|
||||
post.message = this.state.messageText;
|
||||
|
||||
if (post.message.trim().length === 0 && this.state.previews.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (post.message.length > Constants.CHARACTER_LIMIT) {
|
||||
this.setState({postError: `Post length must be less than ${Constants.CHARACTER_LIMIT} characters.`});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({submitting: true, serverError: null});
|
||||
|
||||
if (post.message.indexOf('/') === 0) {
|
||||
Client.executeCommand(
|
||||
this.state.channelId,
|
||||
post.message,
|
||||
false,
|
||||
(data) => {
|
||||
PostStore.storeDraft(this.state.channelId, null);
|
||||
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
|
||||
|
||||
if (data.goto_location && data.goto_location.length > 0) {
|
||||
window.location.href = data.goto_location;
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
if (err.sendMessage) {
|
||||
this.sendMessage(post);
|
||||
} else {
|
||||
const state = {};
|
||||
state.serverError = err.message;
|
||||
state.submitting = false;
|
||||
this.setState(state);
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.sendMessage(post);
|
||||
}
|
||||
}
|
||||
sendMessage(post) {
|
||||
post.channel_id = this.state.channelId;
|
||||
post.filenames = this.state.previews;
|
||||
|
||||
const time = Utils.getTimestamp();
|
||||
const userId = UserStore.getCurrentId();
|
||||
post.pending_post_id = `${userId}:${time}`;
|
||||
post.user_id = userId;
|
||||
post.create_at = time;
|
||||
post.parent_id = this.state.parentId;
|
||||
|
||||
const channel = ChannelStore.get(this.state.channelId);
|
||||
|
||||
GlobalActions.emitUserPostedEvent(post);
|
||||
this.setState({messageText: '', submitting: false, postError: null, previews: [], serverError: null});
|
||||
|
||||
Client.createPost(post, channel,
|
||||
(data) => {
|
||||
AsyncClient.getPosts();
|
||||
|
||||
const member = ChannelStore.getMember(channel.id);
|
||||
member.msg_count = channel.total_msg_count;
|
||||
member.last_viewed_at = Date.now();
|
||||
ChannelStore.setChannelMember(member);
|
||||
|
||||
GlobalActions.emitPostRecievedEvent(data);
|
||||
},
|
||||
(err) => {
|
||||
if (err.id === 'api.post.create_post.root_id.app_error') {
|
||||
// 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);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
submitting: false
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
focusTextbox() {
|
||||
if (!Utils.isMobile()) {
|
||||
this.refs.textbox.focus();
|
||||
}
|
||||
}
|
||||
postMsgKeyPress(e) {
|
||||
if (this.state.ctrlSend && e.ctrlKey || !this.state.ctrlSend) {
|
||||
if (e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
ReactDOM.findDOMNode(this.refs.textbox).blur();
|
||||
this.handleSubmit(e);
|
||||
}
|
||||
}
|
||||
|
||||
const t = Date.now();
|
||||
if ((t - this.lastTime) > Constants.UPDATE_TYPING_MS) {
|
||||
SocketStore.sendMessage({channel_id: this.state.channelId, action: 'typing', props: {parent_id: ''}, state: {}});
|
||||
this.lastTime = t;
|
||||
}
|
||||
}
|
||||
handleUserInput(messageText) {
|
||||
this.setState({messageText});
|
||||
|
||||
const draft = PostStore.getCurrentDraft();
|
||||
draft.message = messageText;
|
||||
PostStore.storeCurrentDraft(draft);
|
||||
}
|
||||
handleUploadClick() {
|
||||
this.focusTextbox();
|
||||
}
|
||||
handleUploadStart(clientIds, channelId) {
|
||||
const draft = PostStore.getDraft(channelId);
|
||||
|
||||
draft.uploadsInProgress = draft.uploadsInProgress.concat(clientIds);
|
||||
PostStore.storeDraft(channelId, draft);
|
||||
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress});
|
||||
|
||||
// this is a bit redundant with the code that sets focus when the file input is clicked,
|
||||
// but this also resets the focus after a drag and drop
|
||||
this.focusTextbox();
|
||||
}
|
||||
handleFileUploadComplete(filenames, clientIds, channelId) {
|
||||
const draft = PostStore.getDraft(channelId);
|
||||
|
||||
// remove each finished file from uploads
|
||||
for (let i = 0; i < clientIds.length; i++) {
|
||||
const index = draft.uploadsInProgress.indexOf(clientIds[i]);
|
||||
|
||||
if (index !== -1) {
|
||||
draft.uploadsInProgress.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
draft.previews = draft.previews.concat(filenames);
|
||||
PostStore.storeDraft(channelId, draft);
|
||||
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
|
||||
}
|
||||
handleUploadError(err, clientId) {
|
||||
let message = err;
|
||||
if (message && typeof message !== 'string') {
|
||||
// err is an AppError from the server
|
||||
message = err.message;
|
||||
}
|
||||
|
||||
if (clientId !== -1) {
|
||||
const draft = PostStore.getDraft(this.state.channelId);
|
||||
|
||||
const index = draft.uploadsInProgress.indexOf(clientId);
|
||||
if (index !== -1) {
|
||||
draft.uploadsInProgress.splice(index, 1);
|
||||
}
|
||||
|
||||
PostStore.storeDraft(this.state.channelId, draft);
|
||||
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress});
|
||||
}
|
||||
|
||||
this.setState({serverError: message});
|
||||
}
|
||||
removePreview(id) {
|
||||
const previews = Object.assign([], this.state.previews);
|
||||
const uploadsInProgress = this.state.uploadsInProgress;
|
||||
|
||||
// id can either be the path of an uploaded file or the client id of an in progress upload
|
||||
let index = previews.indexOf(id);
|
||||
if (index === -1) {
|
||||
index = uploadsInProgress.indexOf(id);
|
||||
|
||||
if (index !== -1) {
|
||||
uploadsInProgress.splice(index, 1);
|
||||
this.refs.fileUpload.getWrappedInstance().cancelUpload(id);
|
||||
}
|
||||
} else {
|
||||
previews.splice(index, 1);
|
||||
}
|
||||
|
||||
const draft = PostStore.getCurrentDraft();
|
||||
draft.previews = previews;
|
||||
draft.uploadsInProgress = uploadsInProgress;
|
||||
PostStore.storeCurrentDraft(draft);
|
||||
|
||||
this.setState({previews, uploadsInProgress});
|
||||
}
|
||||
componentWillMount() {
|
||||
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
|
||||
|
||||
// wait to load these since they may have changed since the component was constructed (particularly in the case of skipping the tutorial)
|
||||
this.setState({
|
||||
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter'),
|
||||
showTutorialTip: tutorialStep === TutorialSteps.POST_POPOVER
|
||||
});
|
||||
}
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onChange);
|
||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||
|
||||
this.focusTextbox();
|
||||
}
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.channelId !== this.state.channelId) {
|
||||
this.focusTextbox();
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onChange);
|
||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||
}
|
||||
onChange() {
|
||||
const channelId = ChannelStore.getCurrentId();
|
||||
if (this.state.channelId !== channelId) {
|
||||
const draft = this.getCurrentDraft();
|
||||
|
||||
this.setState({channelId, messageText: draft.messageText, initialText: draft.messageText, submitting: false, serverError: null, postError: null, previews: draft.previews, uploadsInProgress: draft.uploadsInProgress});
|
||||
}
|
||||
}
|
||||
onPreferenceChange() {
|
||||
const tutorialStep = PreferenceStore.getInt(Preferences.TUTORIAL_STEP, UserStore.getCurrentId(), 999);
|
||||
this.setState({
|
||||
showTutorialTip: tutorialStep === TutorialSteps.POST_POPOVER,
|
||||
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter')
|
||||
});
|
||||
}
|
||||
getFileCount(channelId) {
|
||||
if (channelId === this.state.channelId) {
|
||||
return this.state.previews.length + this.state.uploadsInProgress.length;
|
||||
}
|
||||
|
||||
const draft = PostStore.getDraft(channelId);
|
||||
return draft.previews.length + draft.uploadsInProgress.length;
|
||||
}
|
||||
handleKeyDown(e) {
|
||||
if (this.state.ctrlSend && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
|
||||
this.postMsgKeyPress(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.keyCode === KeyCodes.UP && this.state.messageText === '') {
|
||||
e.preventDefault();
|
||||
|
||||
const channelId = ChannelStore.getCurrentId();
|
||||
const lastPost = PostStore.getCurrentUsersLatestPost(channelId);
|
||||
if (!lastPost) {
|
||||
return;
|
||||
}
|
||||
const {formatMessage} = this.props.intl;
|
||||
var type = (lastPost.root_id && lastPost.root_id.length > 0) ? formatMessage(holders.comment) : formatMessage(holders.post);
|
||||
|
||||
AppDispatcher.handleViewAction({
|
||||
type: ActionTypes.RECEIVED_EDIT_POST,
|
||||
refocusId: '#post_textbox',
|
||||
title: type,
|
||||
message: lastPost.message,
|
||||
postId: lastPost.id,
|
||||
channelId: lastPost.channel_id,
|
||||
comments: PostStore.getCommentCount(lastPost)
|
||||
});
|
||||
}
|
||||
}
|
||||
showPostDeletedModal() {
|
||||
this.setState({
|
||||
showPostDeletedModal: true
|
||||
});
|
||||
}
|
||||
hidePostDeletedModal() {
|
||||
this.setState({
|
||||
showPostDeletedModal: false
|
||||
});
|
||||
}
|
||||
createTutorialTip() {
|
||||
const screens = [];
|
||||
|
||||
screens.push(
|
||||
<div>
|
||||
<FormattedHTMLMessage
|
||||
id='create_post.tutorialTip'
|
||||
defaultMessage='<h4>Sending Messages</h4><p>Type here to write a message and press <strong>Enter</strong> to post it.</p><p>Click the <strong>Attachment</strong> button to upload an image or a file.</p>'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<TutorialTip
|
||||
placement='top'
|
||||
screens={screens}
|
||||
overlayClass='tip-overlay--chat'
|
||||
/>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
let serverError = null;
|
||||
if (this.state.serverError) {
|
||||
serverError = (
|
||||
<div className='has-error'>
|
||||
<label className='control-label'>{this.state.serverError}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let postError = null;
|
||||
if (this.state.postError) {
|
||||
postError = <label className='control-label'>{this.state.postError}</label>;
|
||||
}
|
||||
|
||||
let preview = null;
|
||||
if (this.state.previews.length > 0 || this.state.uploadsInProgress.length > 0) {
|
||||
preview = (
|
||||
<FilePreview
|
||||
files={this.state.previews}
|
||||
onRemove={this.removePreview}
|
||||
uploadsInProgress={this.state.uploadsInProgress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let postFooterClassName = 'post-create-footer';
|
||||
if (postError) {
|
||||
postFooterClassName += ' has-error';
|
||||
}
|
||||
|
||||
let tutorialTip = null;
|
||||
if (this.state.showTutorialTip) {
|
||||
tutorialTip = this.createTutorialTip();
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
id='create_post'
|
||||
ref='topDiv'
|
||||
role='form'
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
<div className='post-create'>
|
||||
<div className='post-create-body'>
|
||||
<div className='post-body__cell'>
|
||||
<Textbox
|
||||
onUserInput={this.handleUserInput}
|
||||
onKeyPress={this.postMsgKeyPress}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
messageText={this.state.messageText}
|
||||
createMessage={this.props.intl.formatMessage(holders.write)}
|
||||
channelId={this.state.channelId}
|
||||
id='post_textbox'
|
||||
ref='textbox'
|
||||
/>
|
||||
<FileUpload
|
||||
ref='fileUpload'
|
||||
getFileCount={this.getFileCount}
|
||||
onClick={this.handleUploadClick}
|
||||
onUploadStart={this.handleUploadStart}
|
||||
onFileUpload={this.handleFileUploadComplete}
|
||||
onUploadError={this.handleUploadError}
|
||||
postType='post'
|
||||
channelId=''
|
||||
/>
|
||||
</div>
|
||||
<a
|
||||
className='send-button theme'
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
<i className='fa fa-paper-plane'/>
|
||||
</a>
|
||||
{tutorialTip}
|
||||
</div>
|
||||
<div className={postFooterClassName}>
|
||||
<MsgTyping
|
||||
channelId={this.state.channelId}
|
||||
parentId=''
|
||||
/>
|
||||
{preview}
|
||||
{postError}
|
||||
{serverError}
|
||||
</div>
|
||||
</div>
|
||||
<PostDeletedModal
|
||||
show={this.state.showPostDeletedModal}
|
||||
onHide={this.hidePostDeletedModal}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CreatePost.propTypes = {
|
||||
intl: intlShape.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(CreatePost);
|
||||
111
webapp/components/delete_channel_modal.jsx
Обычный файл
111
webapp/components/delete_channel_modal.jsx
Обычный файл
@@ -0,0 +1,111 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {browserHistory} from 'react-router';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class DeleteChannelModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleDelete = this.handleDelete.bind(this);
|
||||
}
|
||||
|
||||
handleDelete() {
|
||||
if (this.props.channel.id.length !== 26) {
|
||||
return;
|
||||
}
|
||||
|
||||
browserHistory.push(TeamStore.getCurrentTeamUrl() + '/channels/town-square');
|
||||
Client.deleteChannel(
|
||||
this.props.channel.id,
|
||||
() => {
|
||||
AsyncClient.getChannels(true);
|
||||
},
|
||||
(err) => {
|
||||
AsyncClient.dispatchError(err, 'handleDelete');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let channelTerm = (
|
||||
<FormattedMessage
|
||||
id='delete_channel.channel'
|
||||
defaultMessage='channel'
|
||||
/>
|
||||
);
|
||||
if (this.props.channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
channelTerm = (
|
||||
<FormattedMessage
|
||||
id='delete_channel.group'
|
||||
defaultMessage='group'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.props.onHide}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<h4 className='modal-title'>
|
||||
<FormattedMessage
|
||||
id='delete_channel.confirm'
|
||||
defaultMessage='Confirm DELETE Channel'
|
||||
/>
|
||||
</h4>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<FormattedMessage
|
||||
id='delete_channel.question'
|
||||
defaultMessage='Are you sure you wish to delete the {display_name} {term}?'
|
||||
values={{
|
||||
display_name: this.props.channel.display_name,
|
||||
term: (channelTerm)
|
||||
}}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.props.onHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='delete_channel.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-danger'
|
||||
data-dismiss='modal'
|
||||
onClick={this.handleDelete}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='delete_channel.del'
|
||||
defaultMessage='Delete'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeleteChannelModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired,
|
||||
channel: React.PropTypes.object.isRequired
|
||||
};
|
||||
178
webapp/components/delete_post_modal.jsx
Обычный файл
178
webapp/components/delete_post_modal.jsx
Обычный файл
@@ -0,0 +1,178 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import ModalStore from 'stores/modal_store.jsx';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class DeletePostModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleDelete = this.handleDelete.bind(this);
|
||||
this.handleToggle = this.handleToggle.bind(this);
|
||||
this.handleHide = this.handleHide.bind(this);
|
||||
|
||||
this.state = {
|
||||
show: false,
|
||||
post: null,
|
||||
commentCount: 0,
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ModalStore.addModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ModalStore.removeModalListener(ActionTypes.TOGGLE_DELETE_POST_MODAL, this.handleToggle);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.show && !prevState.show) {
|
||||
setTimeout(() => {
|
||||
$(ReactDOM.findDOMNode(this.refs.deletePostBtn)).focus();
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
handleDelete() {
|
||||
Client.deletePost(
|
||||
this.state.post.channel_id,
|
||||
this.state.post.id,
|
||||
() => {
|
||||
PostStore.deletePost(this.state.post);
|
||||
AsyncClient.getPosts(this.state.post.channel_id);
|
||||
|
||||
if (this.state.post.id === PostStore.getSelectedPostId()) {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_POST_SELECTED,
|
||||
postId: null
|
||||
});
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
AsyncClient.dispatchError(err, 'deletePost');
|
||||
}
|
||||
);
|
||||
|
||||
this.handleHide();
|
||||
}
|
||||
|
||||
handleToggle(value, args) {
|
||||
this.setState({
|
||||
show: value,
|
||||
post: args.post,
|
||||
commentCount: args.commentCount,
|
||||
error: ''
|
||||
});
|
||||
}
|
||||
|
||||
handleHide() {
|
||||
this.setState({show: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.post) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var error = null;
|
||||
if (this.state.error) {
|
||||
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
|
||||
}
|
||||
|
||||
var commentWarning = '';
|
||||
if (this.state.commentCount > 0) {
|
||||
commentWarning = (
|
||||
<FormattedMessage
|
||||
id='delete_post.warning'
|
||||
defaultMessage='This post has {count} comment(s) on it.'
|
||||
values={{
|
||||
count: this.state.commentCount
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const postTerm = this.state.post.root_id ? (
|
||||
<FormattedMessage
|
||||
id='delete_post.comment'
|
||||
defaultMessage='Comment'
|
||||
/>
|
||||
) : (
|
||||
<FormattedMessage
|
||||
id='delete_post.post'
|
||||
defaultMessage='Post'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.state.show}
|
||||
onHide={this.handleHide}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='delete_post.confirm'
|
||||
defaultMessage='Confirm {term} Delete'
|
||||
values={{
|
||||
term: (postTerm)
|
||||
}}
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<FormattedMessage
|
||||
id='delete_post.question'
|
||||
defaultMessage='Are you sure you want to delete this {term}?'
|
||||
values={{
|
||||
term: (postTerm)
|
||||
}}
|
||||
/>
|
||||
<br/>
|
||||
<br/>
|
||||
{commentWarning}
|
||||
{error}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.handleHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='delete_post.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
ref='deletePostBtn'
|
||||
type='button'
|
||||
className='btn btn-danger'
|
||||
onClick={this.handleDelete}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='delete_post.del'
|
||||
defaultMessage='Delete'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
84
webapp/components/do_verify_email.jsx
Обычный файл
84
webapp/components/do_verify_email.jsx
Обычный файл
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
|
||||
import {browserHistory} from 'react-router';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class DoVerifyEmail extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
verifyStatus: 'pending',
|
||||
serverError: ''
|
||||
};
|
||||
}
|
||||
componentWillMount() {
|
||||
const uid = this.props.location.query.uid;
|
||||
const hid = this.props.location.query.hid;
|
||||
const teamName = this.props.location.query.teamname;
|
||||
const email = this.props.location.query.email;
|
||||
|
||||
Client.verifyEmail(
|
||||
() => {
|
||||
browserHistory.push('/' + teamName + '/login?extra=verified&email=' + email);
|
||||
},
|
||||
(err) => {
|
||||
this.setState({verifyStatus: 'failure', serverError: err.message});
|
||||
},
|
||||
uid,
|
||||
hid
|
||||
);
|
||||
}
|
||||
render() {
|
||||
if (this.state.verifyStatus !== 'failure') {
|
||||
return (<LoadingScreen/>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='signup-header'>
|
||||
<a href='/'>
|
||||
<span className='fa fa-chevron-left'/>
|
||||
<FormattedMessage
|
||||
id='web.header.back'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className='col-sm-12'>
|
||||
<div className='signup-team__container'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='email_verify.almost'
|
||||
defaultMessage='{siteName}: You are almost done'
|
||||
values={{
|
||||
siteName: global.window.mm_config.SiteName
|
||||
}}
|
||||
/>
|
||||
</h3>
|
||||
<div>
|
||||
<p>
|
||||
<FormattedMessage id='email_verify.verifyFailed'/>
|
||||
</p>
|
||||
<p className='alert alert-danger'>
|
||||
<i className='fa fa-times'/>
|
||||
{this.state.serverError}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DoVerifyEmail.defaultProps = {
|
||||
};
|
||||
DoVerifyEmail.propTypes = {
|
||||
location: React.PropTypes.object.isRequired
|
||||
};
|
||||
176
webapp/components/edit_channel_header_modal.jsx
Обычный файл
176
webapp/components/edit_channel_header_modal.jsx
Обычный файл
@@ -0,0 +1,176 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
const holders = defineMessages({
|
||||
error: {
|
||||
id: 'edit_channel_header_modal.error',
|
||||
defaultMessage: 'This channel header is too long, please enter a shorter one'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class EditChannelHeaderModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.onShow = this.onShow.bind(this);
|
||||
this.onHide = this.onHide.bind(this);
|
||||
|
||||
this.state = {
|
||||
header: props.channel.header,
|
||||
serverError: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.props.show) {
|
||||
this.onShow();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.channel.header !== nextProps.channel.header) {
|
||||
this.setState({
|
||||
header: nextProps.channel.header
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.show && !prevProps.show) {
|
||||
this.onShow();
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
this.setState({
|
||||
header: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
Client.updateChannelHeader(
|
||||
this.props.channel.id,
|
||||
this.state.header,
|
||||
(channel) => {
|
||||
this.setState({serverError: ''});
|
||||
this.onHide();
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: Constants.ActionTypes.RECEIVED_CHANNEL,
|
||||
channel
|
||||
});
|
||||
},
|
||||
(err) => {
|
||||
if (err.id === 'api.context.invalid_param.app_error') {
|
||||
this.setState({serverError: this.props.intl.formatMessage(holders.error)});
|
||||
} else {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onShow() {
|
||||
const textarea = ReactDOM.findDOMNode(this.refs.textarea);
|
||||
Utils.placeCaretAtEnd(textarea);
|
||||
}
|
||||
|
||||
onHide() {
|
||||
this.setState({
|
||||
serverError: '',
|
||||
header: this.props.channel.header
|
||||
});
|
||||
|
||||
this.props.onHide();
|
||||
}
|
||||
|
||||
render() {
|
||||
var serverError = null;
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><br/><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.onHide}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='edit_channel_header_modal.title'
|
||||
defaultMessage='Edit Header for {channel}'
|
||||
values={{
|
||||
channel: this.props.channel.display_name
|
||||
}}
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='edit_channel_header_modal.description'
|
||||
defaultMessage='Edit the text appearing next to the channel name in the channel header.'
|
||||
/>
|
||||
</p>
|
||||
<textarea
|
||||
ref='textarea'
|
||||
className='form-control no-resize'
|
||||
rows='6'
|
||||
id='edit_header'
|
||||
maxLength='1024'
|
||||
value={this.state.header}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
{serverError}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.onHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='edit_channel_header_modal.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='edit_channel_header_modal.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditChannelHeaderModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired,
|
||||
channel: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(EditChannelHeaderModal);
|
||||
188
webapp/components/edit_channel_purpose_modal.jsx
Обычный файл
188
webapp/components/edit_channel_purpose_modal.jsx
Обычный файл
@@ -0,0 +1,188 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
const holders = defineMessages({
|
||||
error: {
|
||||
id: 'edit_channel_purpose_modal.error',
|
||||
defaultMessage: 'This channel purpose is too long, please enter a shorter one'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class EditChannelPurposeModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleHide = this.handleHide.bind(this);
|
||||
this.handleSave = this.handleSave.bind(this);
|
||||
|
||||
this.state = {serverError: ''};
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.show) {
|
||||
$(ReactDOM.findDOMNode(this.refs.purpose)).focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleHide() {
|
||||
this.setState({serverError: ''});
|
||||
|
||||
if (this.props.onModalDismissed) {
|
||||
this.props.onModalDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
handleSave() {
|
||||
if (!this.props.channel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = {
|
||||
channel_id: this.props.channel.id,
|
||||
channel_purpose: ReactDOM.findDOMNode(this.refs.purpose).value.trim()
|
||||
};
|
||||
|
||||
Client.updateChannelPurpose(data,
|
||||
() => {
|
||||
AsyncClient.getChannel(this.props.channel.id);
|
||||
|
||||
this.handleHide();
|
||||
},
|
||||
(err) => {
|
||||
if (err.id === 'api.context.invalid_param.app_error') {
|
||||
this.setState({serverError: this.props.intl.formatMessage(holders.error)});
|
||||
} else {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let serverError = null;
|
||||
if (this.state.serverError) {
|
||||
serverError = (
|
||||
<div className='form-group has-error'>
|
||||
<br/>
|
||||
<label className='control-label'>{this.state.serverError}</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let title = (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='edit_channel_purpose_modal.title1'
|
||||
defaultMessage='Edit Purpose'
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
if (this.props.channel.display_name) {
|
||||
title = (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='edit_channel_purpose_modal.title2'
|
||||
defaultMessage='Edit Purpose for '
|
||||
/>
|
||||
<span className='name'>{this.props.channel.display_name}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let channelType = (
|
||||
<FormattedMessage
|
||||
id='edit_channel_purpose_modal.channel'
|
||||
defaultMessage='Channel'
|
||||
/>
|
||||
);
|
||||
if (this.props.channel.type === Constants.PRIVATE_CHANNEL) {
|
||||
channelType = (
|
||||
<FormattedMessage
|
||||
id='edit_channel_purpose_modal.group'
|
||||
defaultMessage='Group'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className='modal-edit-channel-purpose'
|
||||
ref='modal'
|
||||
show={this.props.show}
|
||||
onHide={this.handleHide}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
{title}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='edit_channel_purpose_modal.body'
|
||||
defaultMessage='Describe how this {type} should be used. This text appears in the channel list in the "More..." menu and helps others decide whether to join.'
|
||||
values={{
|
||||
type: (channelType)
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<textarea
|
||||
ref='purpose'
|
||||
className='form-control no-resize'
|
||||
rows='6'
|
||||
maxLength='128'
|
||||
defaultValue={this.props.channel.purpose}
|
||||
/>
|
||||
{serverError}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.handleHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='edit_channel_purpose_modal.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
onClick={this.handleSave}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='edit_channel_purpose_modal.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditChannelPurposeModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
channel: React.PropTypes.object,
|
||||
onModalDismissed: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(EditChannelPurposeModal);
|
||||
224
webapp/components/edit_post_modal.jsx
Обычный файл
224
webapp/components/edit_post_modal.jsx
Обычный файл
@@ -0,0 +1,224 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
import Textbox from './textbox.jsx';
|
||||
import BrowserStore from 'stores/browser_store.jsx';
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
var KeyCodes = Constants.KeyCodes;
|
||||
|
||||
const holders = defineMessages({
|
||||
editPost: {
|
||||
id: 'edit_post.editPost',
|
||||
defaultMessage: 'Edit the post...'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class EditPostModal extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.handleEdit = this.handleEdit.bind(this);
|
||||
this.handleEditInput = this.handleEditInput.bind(this);
|
||||
this.handleEditKeyPress = this.handleEditKeyPress.bind(this);
|
||||
this.handleUserInput = this.handleUserInput.bind(this);
|
||||
this.handleEditPostEvent = this.handleEditPostEvent.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.onPreferenceChange = this.onPreferenceChange.bind(this);
|
||||
|
||||
this.state = {editText: '', title: '', post_id: '', channel_id: '', comments: 0, refocusId: ''};
|
||||
}
|
||||
handleEdit() {
|
||||
var updatedPost = {};
|
||||
updatedPost.message = this.state.editText.trim();
|
||||
|
||||
if (updatedPost.message.length === 0) {
|
||||
var tempState = this.state;
|
||||
delete tempState.editText;
|
||||
BrowserStore.setItem('edit_state_transfer', tempState);
|
||||
$('#edit_post').modal('hide');
|
||||
GlobalActions.showDeletePostModal(PostStore.getPost(this.state.channel_id, this.state.post_id), this.state.comments);
|
||||
return;
|
||||
}
|
||||
|
||||
updatedPost.id = this.state.post_id;
|
||||
updatedPost.channel_id = this.state.channel_id;
|
||||
|
||||
Client.updatePost(updatedPost,
|
||||
function success() {
|
||||
AsyncClient.getPosts(updatedPost.channel_id);
|
||||
window.scrollTo(0, 0);
|
||||
},
|
||||
function error(err) {
|
||||
AsyncClient.dispatchError(err, 'updatePost');
|
||||
}
|
||||
);
|
||||
|
||||
$('#edit_post').modal('hide');
|
||||
}
|
||||
handleEditInput(editMessage) {
|
||||
this.setState({editText: editMessage});
|
||||
}
|
||||
handleEditKeyPress(e) {
|
||||
if (!this.state.ctrlSend && e.which === KeyCodes.ENTER && !e.shiftKey && !e.altKey) {
|
||||
e.preventDefault();
|
||||
ReactDOM.findDOMNode(this.refs.editbox).blur();
|
||||
this.handleEdit(e);
|
||||
}
|
||||
}
|
||||
handleUserInput(e) {
|
||||
this.setState({editText: e.target.value});
|
||||
}
|
||||
handleEditPostEvent(options) {
|
||||
this.setState({
|
||||
editText: options.message || '',
|
||||
title: options.title || '',
|
||||
post_id: options.postId || '',
|
||||
channel_id: options.channelId || '',
|
||||
comments: options.comments || 0,
|
||||
refocusId: options.refocusId || ''
|
||||
});
|
||||
|
||||
$(ReactDOM.findDOMNode(this.refs.modal)).modal('show');
|
||||
}
|
||||
handleKeyDown(e) {
|
||||
if (this.state.ctrlSend && e.keyCode === KeyCodes.ENTER && e.ctrlKey === true) {
|
||||
this.handleEdit(e);
|
||||
}
|
||||
}
|
||||
onPreferenceChange() {
|
||||
this.setState({
|
||||
ctrlSend: PreferenceStore.getBool(Constants.Preferences.CATEGORY_ADVANCED_SETTINGS, 'send_on_ctrl_enter')
|
||||
});
|
||||
}
|
||||
componentDidMount() {
|
||||
var self = this;
|
||||
|
||||
$(ReactDOM.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function onHidden() {
|
||||
self.setState({editText: '', title: '', channel_id: '', post_id: '', comments: 0, refocusId: '', error: ''});
|
||||
});
|
||||
|
||||
$(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', function onShow(e) {
|
||||
var button = e.relatedTarget;
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
self.setState({editText: $(button).attr('data-message'), title: $(button).attr('data-title'), channel_id: $(button).attr('data-channelid'), post_id: $(button).attr('data-postid'), comments: $(button).attr('data-comments'), refocusId: $(button).attr('data-refocusid')});
|
||||
});
|
||||
|
||||
$(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', function onShown() {
|
||||
self.refs.editbox.resize();
|
||||
$('#edit_textbox').get(0).focus();
|
||||
});
|
||||
|
||||
$(ReactDOM.findDOMNode(this.refs.modal)).on('hide.bs.modal', function onShown() {
|
||||
if (self.state.refocusId !== '') {
|
||||
setTimeout(() => {
|
||||
$(self.state.refocusId).get(0).focus();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
PostStore.addEditPostListener(this.handleEditPostEvent);
|
||||
PreferenceStore.addChangeListener(this.onPreferenceChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
PostStore.removeEditPostListner(this.handleEditPostEvent);
|
||||
PreferenceStore.removeChangeListener(this.onPreferenceChange);
|
||||
}
|
||||
render() {
|
||||
var error = (<div className='form-group'><br/></div>);
|
||||
if (this.state.error) {
|
||||
error = (<div className='form-group has-error'><br/><label className='control-label'>{this.state.error}</label></div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='modal fade edit-modal'
|
||||
ref='modal'
|
||||
id='edit_post'
|
||||
role='dialog'
|
||||
tabIndex='-1'
|
||||
aria-hidden='true'
|
||||
>
|
||||
<div className='modal-dialog modal-push-down'>
|
||||
<div className='modal-content'>
|
||||
<div className='modal-header'>
|
||||
<button
|
||||
type='button'
|
||||
className='close'
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
onClick={this.handleEditClose}
|
||||
>
|
||||
<span aria-hidden='true'>×</span>
|
||||
</button>
|
||||
<h4 className='modal-title'>
|
||||
<FormattedMessage
|
||||
id='edit_post.edit'
|
||||
defaultMessage='Edit {title}'
|
||||
values={{
|
||||
title: this.state.title
|
||||
}}
|
||||
/>
|
||||
</h4>
|
||||
</div>
|
||||
<div className='edit-modal-body modal-body'>
|
||||
<Textbox
|
||||
onUserInput={this.handleEditInput}
|
||||
onKeyPress={this.handleEditKeyPress}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
messageText={this.state.editText}
|
||||
createMessage={this.props.intl.formatMessage(holders.editPost)}
|
||||
supportsCommands={false}
|
||||
id='edit_textbox'
|
||||
ref='editbox'
|
||||
/>
|
||||
{error}
|
||||
</div>
|
||||
<div className='modal-footer'>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
data-dismiss='modal'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='edit_post.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
onClick={this.handleEdit}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='edit_post.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
EditPostModal.propTypes = {
|
||||
intl: intlShape.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(EditPostModal);
|
||||
85
webapp/components/error_bar.jsx
Обычный файл
85
webapp/components/error_bar.jsx
Обычный файл
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ErrorStore from 'stores/error_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class ErrorBar extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onErrorChange = this.onErrorChange.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
|
||||
this.state = ErrorStore.getLastError();
|
||||
}
|
||||
|
||||
isValidError(s) {
|
||||
if (!s) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!s.message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
if (global.window.mm_config.SendEmailNotifications === 'false') {
|
||||
ErrorStore.storeLastError({message: Utils.localizeMessage('error_bar.preview_mode', 'Preview Mode: Email notifications have not been configured')});
|
||||
this.onErrorChange();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ErrorStore.addChangeListener(this.onErrorChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ErrorStore.removeChangeListener(this.onErrorChange);
|
||||
}
|
||||
|
||||
onErrorChange() {
|
||||
var newState = ErrorStore.getLastError();
|
||||
|
||||
if (newState) {
|
||||
this.setState(newState);
|
||||
} else {
|
||||
this.setState({message: null});
|
||||
}
|
||||
}
|
||||
|
||||
handleClose(e) {
|
||||
if (e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
ErrorStore.clearLastError();
|
||||
this.setState({message: null});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isValidError(this.state)) {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='error-bar'>
|
||||
<span>{this.state.message}</span>
|
||||
<a
|
||||
href='#'
|
||||
className='error-bar__close'
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
×
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBar;
|
||||
228
webapp/components/file_attachment.jsx
Обычный файл
228
webapp/components/file_attachment.jsx
Обычный файл
@@ -0,0 +1,228 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as utils from 'utils/utils.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
download: {
|
||||
id: 'file_attachment.download',
|
||||
defaultMessage: 'Download'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class FileAttachment extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.loadFiles = this.loadFiles.bind(this);
|
||||
this.addBackgroundImage = this.addBackgroundImage.bind(this);
|
||||
|
||||
this.canSetState = false;
|
||||
this.state = {fileSize: -1};
|
||||
}
|
||||
componentDidMount() {
|
||||
this.loadFiles();
|
||||
}
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.filename !== prevProps.filename) {
|
||||
this.loadFiles();
|
||||
}
|
||||
}
|
||||
loadFiles() {
|
||||
this.canSetState = true;
|
||||
|
||||
var filename = this.props.filename;
|
||||
|
||||
if (filename) {
|
||||
var fileInfo = this.getFileInfoFromName(filename);
|
||||
var type = utils.getFileType(fileInfo.ext);
|
||||
|
||||
if (type === 'image') {
|
||||
var self = this; // Need this reference since we use the given "this"
|
||||
$('<img/>').attr('src', fileInfo.path + '_thumb.jpg').load(function loadWrapper(path, name) {
|
||||
return function loader() {
|
||||
$(this).remove();
|
||||
if (name in self.refs) {
|
||||
var imgDiv = ReactDOM.findDOMNode(self.refs[name]);
|
||||
|
||||
$(imgDiv).removeClass('post-image__load');
|
||||
$(imgDiv).addClass('post-image');
|
||||
|
||||
var width = this.width || $(this).width();
|
||||
var height = this.height || $(this).height();
|
||||
|
||||
if (width < Constants.THUMBNAIL_WIDTH &&
|
||||
height < Constants.THUMBNAIL_HEIGHT) {
|
||||
$(imgDiv).addClass('small');
|
||||
} else {
|
||||
$(imgDiv).addClass('normal');
|
||||
}
|
||||
|
||||
self.addBackgroundImage(name, path);
|
||||
}
|
||||
};
|
||||
}(fileInfo.path, filename));
|
||||
}
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
// keep track of when this component is mounted so that we can asynchronously change state without worrying about whether or not we're mounted
|
||||
this.canSetState = false;
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!utils.areObjectsEqual(nextProps, this.props)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// the only time this object should update is when it receives an updated file size which we can usually handle without re-rendering
|
||||
if (nextState.fileSize !== this.state.fileSize) {
|
||||
if (this.refs.fileSize) {
|
||||
// update the UI element to display the file size without re-rendering the whole component
|
||||
ReactDOM.findDOMNode(this.refs.fileSize).innerHTML = utils.fileSizeToString(nextState.fileSize);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// we can't find the element that should hold the file size so we must not have rendered yet
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
getFileInfoFromName(name) {
|
||||
var fileInfo = utils.splitFileLocation(name);
|
||||
|
||||
fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
|
||||
|
||||
return fileInfo;
|
||||
}
|
||||
addBackgroundImage(name, path) {
|
||||
var fileUrl = path;
|
||||
|
||||
if (name in this.refs) {
|
||||
if (!path) {
|
||||
fileUrl = this.getFileInfoFromName(name).path;
|
||||
}
|
||||
|
||||
var imgDiv = ReactDOM.findDOMNode(this.refs[name]);
|
||||
var re1 = new RegExp(' ', 'g');
|
||||
var re2 = new RegExp('\\(', 'g');
|
||||
var re3 = new RegExp('\\)', 'g');
|
||||
var url = fileUrl.replace(re1, '%20').replace(re2, '%28').replace(re3, '%29');
|
||||
|
||||
$(imgDiv).css('background-image', 'url(' + url + '_thumb.jpg)');
|
||||
}
|
||||
}
|
||||
removeBackgroundImage(name) {
|
||||
if (name in this.refs) {
|
||||
$(ReactDOM.findDOMNode(this.refs[name])).css('background-image', 'initial');
|
||||
}
|
||||
}
|
||||
render() {
|
||||
var filename = this.props.filename;
|
||||
|
||||
var fileInfo = utils.splitFileLocation(filename);
|
||||
var fileUrl = utils.getFileUrl(filename, true);
|
||||
var type = utils.getFileType(fileInfo.ext);
|
||||
|
||||
var thumbnail;
|
||||
if (type === 'image') {
|
||||
thumbnail = (
|
||||
<div
|
||||
ref={filename}
|
||||
className='post-image__load'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
thumbnail = <div className={'file-icon ' + utils.getIconClassName(type)}/>;
|
||||
}
|
||||
|
||||
var fileSizeString = '';
|
||||
if (this.state.fileSize < 0) {
|
||||
Client.getFileInfo(
|
||||
filename,
|
||||
function success(data) {
|
||||
if (this.canSetState) {
|
||||
this.setState({fileSize: parseInt(data.size, 10)});
|
||||
}
|
||||
}.bind(this),
|
||||
function error() {
|
||||
// Do nothing
|
||||
}
|
||||
);
|
||||
} else {
|
||||
fileSizeString = utils.fileSizeToString(this.state.fileSize);
|
||||
}
|
||||
|
||||
var filenameString = decodeURIComponent(utils.getFileName(filename));
|
||||
var trimmedFilename;
|
||||
if (filenameString.length > 35) {
|
||||
trimmedFilename = filenameString.substring(0, Math.min(35, filenameString.length)) + '...';
|
||||
} else {
|
||||
trimmedFilename = filenameString;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='post-image__column'
|
||||
key={filename}
|
||||
>
|
||||
<a className='post-image__thumbnail'
|
||||
href='#'
|
||||
onClick={() => this.props.handleImageClick(this.props.index)}
|
||||
>
|
||||
{thumbnail}
|
||||
</a>
|
||||
<div className='post-image__details'>
|
||||
<a
|
||||
href={fileUrl}
|
||||
download={filenameString}
|
||||
data-toggle='tooltip'
|
||||
title={this.props.intl.formatMessage(holders.download) + ' \"' + filenameString + '\"'}
|
||||
className='post-image__name'
|
||||
target='_blank'
|
||||
>
|
||||
{trimmedFilename}
|
||||
</a>
|
||||
<div>
|
||||
<a
|
||||
href={fileUrl}
|
||||
download={filenameString}
|
||||
className='post-image__download'
|
||||
target='_blank'
|
||||
>
|
||||
<span
|
||||
className='fa fa-download'
|
||||
/>
|
||||
</a>
|
||||
<span className='post-image__type'>{fileInfo.ext.toUpperCase()}</span>
|
||||
<span className='post-image__size'>{fileSizeString}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileAttachment.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
|
||||
// a list of file pathes displayed by the parent FileAttachmentList
|
||||
filename: React.PropTypes.string.isRequired,
|
||||
|
||||
// the index of this attachment preview in the parent FileAttachmentList
|
||||
index: React.PropTypes.number.isRequired,
|
||||
|
||||
// handler for when the thumbnail is clicked passed the index above
|
||||
handleImageClick: React.PropTypes.func
|
||||
};
|
||||
|
||||
export default injectIntl(FileAttachment);
|
||||
64
webapp/components/file_attachment_list.jsx
Обычный файл
64
webapp/components/file_attachment_list.jsx
Обычный файл
@@ -0,0 +1,64 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ViewImageModal from './view_image.jsx';
|
||||
import FileAttachment from './file_attachment.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class FileAttachmentList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleImageClick = this.handleImageClick.bind(this);
|
||||
|
||||
this.state = {showPreviewModal: false, startImgId: 0};
|
||||
}
|
||||
handleImageClick(indexClicked) {
|
||||
this.setState({showPreviewModal: true, startImgId: indexClicked});
|
||||
}
|
||||
render() {
|
||||
var filenames = this.props.filenames;
|
||||
|
||||
var postFiles = [];
|
||||
for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) {
|
||||
postFiles.push(
|
||||
<FileAttachment
|
||||
key={'file_attachment_' + i}
|
||||
filename={filenames[i]}
|
||||
index={i}
|
||||
handleImageClick={this.handleImageClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='post-image__columns'>
|
||||
{postFiles}
|
||||
</div>
|
||||
<ViewImageModal
|
||||
show={this.state.showPreviewModal}
|
||||
onModalDismissed={() => this.setState({showPreviewModal: false})}
|
||||
channelId={this.props.channelId}
|
||||
userId={this.props.userId}
|
||||
startId={this.state.startImgId}
|
||||
filenames={filenames}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileAttachmentList.propTypes = {
|
||||
|
||||
// a list of file pathes displayed by this
|
||||
filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
|
||||
|
||||
// the channel that this is part of
|
||||
channelId: React.PropTypes.string,
|
||||
|
||||
// the user that owns the post that this is attached to
|
||||
userId: React.PropTypes.string
|
||||
};
|
||||
57
webapp/components/file_info_preview.jsx
Обычный файл
57
webapp/components/file_info_preview.jsx
Обычный файл
@@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {defineMessages} from 'react-intl';
|
||||
import React from 'react';
|
||||
|
||||
const holders = defineMessages({
|
||||
type: {
|
||||
id: 'file_info_preview.type',
|
||||
defaultMessage: 'File type '
|
||||
},
|
||||
size: {
|
||||
id: 'file_info_preview.size',
|
||||
defaultMessage: 'Size '
|
||||
}
|
||||
});
|
||||
|
||||
export default function FileInfoPreview({filename, fileUrl, fileInfo, formatMessage}) {
|
||||
// non-image files include a section providing details about the file
|
||||
const infoParts = [];
|
||||
|
||||
if (fileInfo.extension !== '') {
|
||||
infoParts.push(formatMessage(holders.type) + fileInfo.extension.toUpperCase());
|
||||
}
|
||||
|
||||
infoParts.push(formatMessage(holders.size) + Utils.fileSizeToString(fileInfo.size));
|
||||
|
||||
const infoString = infoParts.join(', ');
|
||||
|
||||
const name = decodeURIComponent(Utils.getFileName(filename));
|
||||
|
||||
return (
|
||||
<div className='file-details__container'>
|
||||
<a
|
||||
className={'file-details__preview'}
|
||||
href={fileUrl}
|
||||
target='_blank'
|
||||
>
|
||||
<span className='file-details__preview-helper'/>
|
||||
<img src={Utils.getPreviewImagePath(filename)}/>
|
||||
</a>
|
||||
<div className='file-details'>
|
||||
<div className='file-details__name'>{name}</div>
|
||||
<div className='file-details__info'>{infoString}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FileInfoPreview.propTypes = {
|
||||
filename: React.PropTypes.string.isRequired,
|
||||
fileUrl: React.PropTypes.string.isRequired,
|
||||
fileInfo: React.PropTypes.object.isRequired,
|
||||
formatMessage: React.PropTypes.func.isRequired
|
||||
};
|
||||
121
webapp/components/file_preview.jsx
Обычный файл
121
webapp/components/file_preview.jsx
Обычный файл
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import loadingGif from 'images/load.gif';
|
||||
|
||||
export default class FilePreview extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleRemove = this.handleRemove.bind(this);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.props.uploadsInProgress.length > 0) {
|
||||
ReactDOM.findDOMNode(this.refs[this.props.uploadsInProgress[0]]).scrollIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
handleRemove(e) {
|
||||
var previewDiv = e.target.parentNode.parentNode;
|
||||
|
||||
if (previewDiv.hasAttribute('data-filename')) {
|
||||
this.props.onRemove(previewDiv.getAttribute('data-filename'));
|
||||
} else if (previewDiv.hasAttribute('data-client-id')) {
|
||||
this.props.onRemove(previewDiv.getAttribute('data-client-id'));
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var previews = [];
|
||||
this.props.files.forEach((fullFilename) => {
|
||||
var filename = fullFilename;
|
||||
var originalFilename = filename;
|
||||
var filenameSplit = filename.split('.');
|
||||
var ext = filenameSplit[filenameSplit.length - 1];
|
||||
var type = Utils.getFileType(ext);
|
||||
|
||||
filename = Utils.getFileUrl(filename);
|
||||
|
||||
if (type === 'image') {
|
||||
previews.push(
|
||||
<div
|
||||
key={filename}
|
||||
className='file-preview'
|
||||
data-filename={originalFilename}
|
||||
>
|
||||
<img
|
||||
className='file-preview__image'
|
||||
src={filename}
|
||||
/>
|
||||
<a
|
||||
className='file-preview__remove'
|
||||
onClick={this.handleRemove}
|
||||
>
|
||||
<i className='glyphicon glyphicon-remove'/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
previews.push(
|
||||
<div
|
||||
key={filename}
|
||||
className='file-preview custom-file'
|
||||
data-filename={originalFilename}
|
||||
>
|
||||
<div className={'file-icon ' + Utils.getIconClassName(type)}/>
|
||||
<a
|
||||
className='file-preview__remove'
|
||||
onClick={this.handleRemove}
|
||||
>
|
||||
<i className='glyphicon glyphicon-remove'/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
this.props.uploadsInProgress.forEach((clientId) => {
|
||||
previews.push(
|
||||
<div
|
||||
ref={clientId}
|
||||
key={clientId}
|
||||
className='file-preview'
|
||||
data-client-id={clientId}
|
||||
>
|
||||
<img
|
||||
className='spinner'
|
||||
src={loadingGif}
|
||||
/>
|
||||
<a
|
||||
className='file-preview__remove'
|
||||
onClick={this.handleRemove}
|
||||
>
|
||||
<i className='glyphicon glyphicon-remove'/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='file-preview__container'>
|
||||
{previews}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilePreview.defaultProps = {
|
||||
files: [],
|
||||
uploadsInProgress: []
|
||||
};
|
||||
FilePreview.propTypes = {
|
||||
onRemove: React.PropTypes.func.isRequired,
|
||||
files: React.PropTypes.array,
|
||||
uploadsInProgress: React.PropTypes.array
|
||||
};
|
||||
340
webapp/components/file_upload.jsx
Обычный файл
340
webapp/components/file_upload.jsx
Обычный файл
@@ -0,0 +1,340 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import 'jquery-dragster/jquery.dragster.js';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
limited: {
|
||||
id: 'file_upload.limited',
|
||||
defaultMessage: 'Uploads limited to {count} files maximum. Please use additional posts for more files.'
|
||||
},
|
||||
filesAbove: {
|
||||
id: 'file_upload.filesAbove',
|
||||
defaultMessage: 'Files above {max}MB could not be uploaded: {filenames}'
|
||||
},
|
||||
fileAbove: {
|
||||
id: 'file_upload.fileAbove',
|
||||
defaultMessage: 'File above {max}MB could not be uploaded: {filename}'
|
||||
},
|
||||
pasted: {
|
||||
id: 'file_upload.pasted',
|
||||
defaultMessage: 'Image Pasted at '
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class FileUpload extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
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: {}
|
||||
};
|
||||
}
|
||||
|
||||
fileUploadSuccess(channelId, data) {
|
||||
this.props.onFileUpload(data.filenames, data.client_ids, channelId);
|
||||
|
||||
var requests = this.state.requests;
|
||||
for (var j = 0; j < data.client_ids.length; j++) {
|
||||
delete requests[data.client_ids[j]];
|
||||
}
|
||||
this.setState({requests});
|
||||
}
|
||||
|
||||
fileUploadFail(clientId, err) {
|
||||
this.props.onUploadError(err, clientId);
|
||||
}
|
||||
|
||||
uploadFiles(files) {
|
||||
// clear any existing errors
|
||||
this.props.onUploadError(null);
|
||||
|
||||
var channelId = this.props.channelId || ChannelStore.getCurrentId();
|
||||
|
||||
var uploadsRemaining = Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId);
|
||||
var numUploads = 0;
|
||||
|
||||
// keep track of how many files have been too large
|
||||
var tooLargeFiles = [];
|
||||
|
||||
for (let i = 0; i < files.length && numUploads < uploadsRemaining; i++) {
|
||||
if (files[i].size > Constants.MAX_FILE_SIZE) {
|
||||
tooLargeFiles.push(files[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// generate a unique id that can be used by other components to refer back to this upload
|
||||
const clientId = Utils.generateId();
|
||||
|
||||
// prepare data to be uploaded
|
||||
var formData = new FormData();
|
||||
formData.append('channel_id', channelId);
|
||||
formData.append('files', files[i], files[i].name);
|
||||
formData.append('client_ids', clientId);
|
||||
|
||||
var request = Client.uploadFile(formData,
|
||||
this.fileUploadSuccess.bind(this, channelId),
|
||||
this.fileUploadFail.bind(this, clientId)
|
||||
);
|
||||
|
||||
var requests = this.state.requests;
|
||||
requests[clientId] = request;
|
||||
this.setState({requests});
|
||||
|
||||
this.props.onUploadStart([clientId], channelId);
|
||||
|
||||
numUploads += 1;
|
||||
}
|
||||
|
||||
const {formatMessage} = this.props.intl;
|
||||
if (files.length > uploadsRemaining) {
|
||||
this.props.onUploadError(formatMessage(holders.limited, {count: Constants.MAX_UPLOAD_FILES}));
|
||||
} else if (tooLargeFiles.length > 1) {
|
||||
var tooLargeFilenames = tooLargeFiles.map((file) => file.name).join(', ');
|
||||
|
||||
this.props.onUploadError(formatMessage(holders.filesAbove, {max: (Constants.MAX_FILE_SIZE / 1000000), filenames: tooLargeFilenames}));
|
||||
} else if (tooLargeFiles.length > 0) {
|
||||
this.props.onUploadError(formatMessage(holders.fileAbove, {max: (Constants.MAX_FILE_SIZE / 1000000), filename: tooLargeFiles[0].name}));
|
||||
}
|
||||
}
|
||||
|
||||
handleChange(e) {
|
||||
if (e.target.files.length > 0) {
|
||||
this.uploadFiles(e.target.files);
|
||||
|
||||
Utils.clearFileInput(e.target);
|
||||
}
|
||||
}
|
||||
|
||||
handleDrop(e) {
|
||||
this.props.onUploadError(null);
|
||||
|
||||
var files = e.originalEvent.dataTransfer.files;
|
||||
|
||||
if (typeof files !== 'string' && files.length) {
|
||||
this.uploadFiles(files);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
var inputDiv = ReactDOM.findDOMNode(this.refs.input);
|
||||
var self = this;
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
if (this.props.postType === 'post') {
|
||||
$('.row.main').dragster({
|
||||
enter(dragsterEvent, e) {
|
||||
var files = e.originalEvent.dataTransfer;
|
||||
|
||||
if (Utils.isFileTransfer(files)) {
|
||||
$('.center-file-overlay').removeClass('hidden');
|
||||
}
|
||||
},
|
||||
leave(dragsterEvent, e) {
|
||||
var files = e.originalEvent.dataTransfer;
|
||||
|
||||
if (Utils.isFileTransfer(files)) {
|
||||
$('.center-file-overlay').addClass('hidden');
|
||||
}
|
||||
},
|
||||
drop(dragsterEvent, e) {
|
||||
$('.center-file-overlay').addClass('hidden');
|
||||
self.handleDrop(e);
|
||||
}
|
||||
});
|
||||
} else if (this.props.postType === 'comment') {
|
||||
$('.post-right__container').dragster({
|
||||
enter(dragsterEvent, e) {
|
||||
var files = e.originalEvent.dataTransfer;
|
||||
|
||||
if (Utils.isFileTransfer(files)) {
|
||||
$('.right-file-overlay').removeClass('hidden');
|
||||
}
|
||||
},
|
||||
leave(dragsterEvent, e) {
|
||||
var files = e.originalEvent.dataTransfer;
|
||||
|
||||
if (Utils.isFileTransfer(files)) {
|
||||
$('.right-file-overlay').addClass('hidden');
|
||||
}
|
||||
},
|
||||
drop(dragsterEvent, e) {
|
||||
$('.right-file-overlay').addClass('hidden');
|
||||
self.handleDrop(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('paste', (e) => {
|
||||
if (!e.clipboardData) {
|
||||
return;
|
||||
}
|
||||
|
||||
var textarea = $(inputDiv.parentNode.parentNode).find('.custom-textarea')[0];
|
||||
|
||||
if (textarea !== e.target && !$.contains(textarea, e.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.props.onUploadError(null);
|
||||
|
||||
// This looks redundant, but must be done this way due to
|
||||
// setState being an asynchronous call
|
||||
var items = e.clipboardData.items;
|
||||
var numItems = 0;
|
||||
if (items) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].type.indexOf('image') !== -1) {
|
||||
var testExt = items[i].type.split('/')[1].toLowerCase();
|
||||
|
||||
if (Constants.IMAGE_TYPES.indexOf(testExt) < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
numItems++;
|
||||
}
|
||||
}
|
||||
|
||||
var numToUpload = Math.min(Constants.MAX_UPLOAD_FILES - self.props.getFileCount(ChannelStore.getCurrentId()), numItems);
|
||||
|
||||
if (numItems > numToUpload) {
|
||||
self.props.onUploadError(formatMessage(holders.limited, {count: Constants.MAX_UPLOAD_FILES}));
|
||||
}
|
||||
|
||||
for (var i = 0; i < items.length && i < numToUpload; i++) {
|
||||
if (items[i].type.indexOf('image') !== -1) {
|
||||
var file = items[i].getAsFile();
|
||||
|
||||
var ext = items[i].type.split('/')[1].toLowerCase();
|
||||
|
||||
if (Constants.IMAGE_TYPES.indexOf(ext) < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var channelId = self.props.channelId || ChannelStore.getCurrentId();
|
||||
|
||||
// generate a unique id that can be used by other components to refer back to this file upload
|
||||
var clientId = Utils.generateId();
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('channel_id', channelId);
|
||||
var d = new Date();
|
||||
var hour;
|
||||
if (d.getHours() < 10) {
|
||||
hour = '0' + d.getHours();
|
||||
} else {
|
||||
hour = String(d.getHours());
|
||||
}
|
||||
var min;
|
||||
if (d.getMinutes() < 10) {
|
||||
min = '0' + d.getMinutes();
|
||||
} else {
|
||||
min = String(d.getMinutes());
|
||||
}
|
||||
|
||||
var name = formatMessage(holders.pasted) + d.getFullYear() + '-' + d.getMonth() + '-' + d.getDate() + ' ' + hour + '-' + min + '.' + ext;
|
||||
formData.append('files', file, name);
|
||||
formData.append('client_ids', clientId);
|
||||
|
||||
var request = Client.uploadFile(formData,
|
||||
self.fileUploadSuccess.bind(self, channelId),
|
||||
self.fileUploadFail.bind(self, clientId)
|
||||
);
|
||||
|
||||
var requests = self.state.requests;
|
||||
requests[clientId] = request;
|
||||
self.setState({requests});
|
||||
|
||||
self.props.onUploadStart([clientId], channelId);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
let target;
|
||||
if (this.props.postType === 'post') {
|
||||
target = $('.row.main');
|
||||
} else {
|
||||
target = $('.post-right__container');
|
||||
}
|
||||
|
||||
// jquery-dragster doesn't provide a function to unregister itself so do it manually
|
||||
target.off('dragenter dragleave dragover drop dragster:enter dragster:leave dragster:over dragster:drop');
|
||||
}
|
||||
|
||||
cancelUpload(clientId) {
|
||||
var requests = this.state.requests;
|
||||
var request = requests[clientId];
|
||||
|
||||
if (request) {
|
||||
request.abort();
|
||||
|
||||
delete requests[clientId];
|
||||
this.setState({requests});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let multiple = true;
|
||||
if (Utils.isMobileApp()) {
|
||||
// iOS WebViews don't upload videos properly in multiple mode
|
||||
multiple = false;
|
||||
}
|
||||
|
||||
let accept = '';
|
||||
if (Utils.isIosChrome()) {
|
||||
// iOS Chrome can't upload videos at all
|
||||
accept = 'image/*';
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
ref='input'
|
||||
className='btn btn-file'
|
||||
>
|
||||
<span>
|
||||
<i className='glyphicon glyphicon-paperclip'/>
|
||||
</span>
|
||||
<input
|
||||
ref='fileInput'
|
||||
type='file'
|
||||
onChange={this.handleChange}
|
||||
onClick={this.props.onClick}
|
||||
multiple={multiple}
|
||||
accept={accept}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileUpload.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
onUploadError: React.PropTypes.func,
|
||||
getFileCount: React.PropTypes.func,
|
||||
onClick: React.PropTypes.func,
|
||||
onFileUpload: React.PropTypes.func,
|
||||
onUploadStart: React.PropTypes.func,
|
||||
onTextDrop: React.PropTypes.func,
|
||||
channelId: React.PropTypes.string,
|
||||
postType: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default injectIntl(FileUpload, {withRef: true});
|
||||
50
webapp/components/file_upload_overlay.jsx
Обычный файл
50
webapp/components/file_upload_overlay.jsx
Обычный файл
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import fileOverlayImage from 'images/filesOverlay.png';
|
||||
import overlayLogoImage from 'images/logoWhite.png';
|
||||
|
||||
export default class FileUploadOverlay extends React.Component {
|
||||
render() {
|
||||
var overlayClass = 'file-overlay hidden';
|
||||
if (this.props.overlayType === 'right') {
|
||||
overlayClass += ' right-file-overlay';
|
||||
} else if (this.props.overlayType === 'center') {
|
||||
overlayClass += ' center-file-overlay';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={overlayClass}>
|
||||
<div className='overlay__indent'>
|
||||
<div className='overlay__circle'>
|
||||
<img
|
||||
className='overlay__files'
|
||||
src={fileOverlayImage}
|
||||
alt='Files'
|
||||
/>
|
||||
<span><i className='fa fa-upload'></i>
|
||||
<FormattedMessage
|
||||
id='upload_overlay.info'
|
||||
defaultMessage='Drop a file to upload it.'
|
||||
/>
|
||||
</span>
|
||||
<img
|
||||
className='overlay__logo'
|
||||
src={overlayLogoImage}
|
||||
width='100'
|
||||
alt='Logo'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileUploadOverlay.propTypes = {
|
||||
overlayType: React.PropTypes.string
|
||||
};
|
||||
136
webapp/components/filtered_user_list.jsx
Обычный файл
136
webapp/components/filtered_user_list.jsx
Обычный файл
@@ -0,0 +1,136 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import UserList from './user_list.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
member: {
|
||||
id: 'filtered_user_list.member',
|
||||
defaultMessage: 'Member'
|
||||
},
|
||||
search: {
|
||||
id: 'filtered_user_list.search',
|
||||
defaultMessage: 'Search members'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class FilteredUserList extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleFilterChange = this.handleFilterChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
filter: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (prevState.filter !== this.state.filter) {
|
||||
$(ReactDOM.findDOMNode(this.refs.userList)).scrollTop(0);
|
||||
}
|
||||
}
|
||||
|
||||
handleFilterChange(e) {
|
||||
this.setState({
|
||||
filter: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
let users = this.props.users;
|
||||
|
||||
if (this.state.filter) {
|
||||
const filter = this.state.filter.toLowerCase();
|
||||
|
||||
users = users.filter((user) => {
|
||||
return user.username.toLowerCase().indexOf(filter) !== -1 ||
|
||||
(user.first_name && user.first_name.toLowerCase().indexOf(filter) !== -1) ||
|
||||
(user.last_name && user.last_name.toLowerCase().indexOf(filter) !== -1) ||
|
||||
(user.nickname && user.nickname.toLowerCase().indexOf(filter) !== -1);
|
||||
});
|
||||
}
|
||||
|
||||
let count;
|
||||
if (users.length === this.props.users.length) {
|
||||
count = (
|
||||
<FormattedMessage
|
||||
id='filtered_user_list.count'
|
||||
defaultMessage='{count} {count, plural,
|
||||
one {member}
|
||||
other {members}
|
||||
}'
|
||||
values={{
|
||||
count: users.length
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
count = (
|
||||
<FormattedMessage
|
||||
id='filtered_user_list.countTotal'
|
||||
defaultMessage='{count} {count, plural,
|
||||
one {member}
|
||||
other {members}
|
||||
} of {total} Total'
|
||||
values={{
|
||||
count: users.length,
|
||||
total: this.props.users.length
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='filtered-user-list'
|
||||
style={this.props.style}
|
||||
>
|
||||
<div className='filter-row'>
|
||||
<div className='col-sm-6'>
|
||||
<input
|
||||
ref='filter'
|
||||
className='form-control filter-textbox'
|
||||
placeholder={formatMessage(holders.search)}
|
||||
onInput={this.handleFilterChange}
|
||||
/>
|
||||
</div>
|
||||
<div className='col-sm-6'>
|
||||
<span className='member-count'>{count}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref='userList'
|
||||
className='more-modal__list'
|
||||
>
|
||||
<UserList
|
||||
users={users}
|
||||
actions={this.props.actions}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FilteredUserList.defaultProps = {
|
||||
users: [],
|
||||
actions: []
|
||||
};
|
||||
|
||||
FilteredUserList.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
users: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
actions: React.PropTypes.arrayOf(React.PropTypes.func),
|
||||
style: React.PropTypes.object
|
||||
};
|
||||
|
||||
export default injectIntl(FilteredUserList);
|
||||
139
webapp/components/get_link_modal.jsx
Обычный файл
139
webapp/components/get_link_modal.jsx
Обычный файл
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class GetLinkModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onHide = this.onHide.bind(this);
|
||||
|
||||
this.copyLink = this.copyLink.bind(this);
|
||||
|
||||
this.state = {
|
||||
copiedLink: false
|
||||
};
|
||||
}
|
||||
|
||||
onHide() {
|
||||
this.setState({copiedLink: false});
|
||||
|
||||
this.props.onHide();
|
||||
}
|
||||
|
||||
copyLink() {
|
||||
var copyTextarea = $(ReactDOM.findDOMNode(this.refs.textarea));
|
||||
copyTextarea.select();
|
||||
|
||||
try {
|
||||
var successful = document.execCommand('copy');
|
||||
if (successful) {
|
||||
this.setState({copiedLink: true});
|
||||
} else {
|
||||
this.setState({copiedLink: false});
|
||||
}
|
||||
} catch (err) {
|
||||
this.setState({copiedLink: false});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let helpText = null;
|
||||
if (this.props.helpText) {
|
||||
helpText = (
|
||||
<p>
|
||||
{this.props.helpText}
|
||||
<br/>
|
||||
<br/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
let copyLink = null;
|
||||
if (document.queryCommandSupported('copy')) {
|
||||
copyLink = (
|
||||
<button
|
||||
data-copy-btn='true'
|
||||
type='button'
|
||||
className='btn btn-primary pull-left'
|
||||
onClick={this.copyLink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='get_link.copy'
|
||||
defaultMessage='Copy Link'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const linkText = (
|
||||
<textarea
|
||||
className='form-control no-resize min-height'
|
||||
readOnly='true'
|
||||
ref='textarea'
|
||||
value={this.props.link}
|
||||
/>
|
||||
);
|
||||
|
||||
var copyLinkConfirm = null;
|
||||
if (this.state.copiedLink) {
|
||||
copyLinkConfirm = (
|
||||
<p className='alert alert-success alert--confirm'>
|
||||
<i className='fa fa-check'></i>
|
||||
<FormattedMessage
|
||||
id='get_link.clipboard'
|
||||
defaultMessage=' Link copied to clipboard.'
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
onHide={this.onHide}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<h4 className='modal-title'>{this.props.title}</h4>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{helpText}
|
||||
{linkText}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.onHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='get_link.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</button>
|
||||
{copyLink}
|
||||
{copyLinkConfirm}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GetLinkModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
helpText: React.PropTypes.string,
|
||||
link: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
GetLinkModal.defaultProps = {
|
||||
helpText: null
|
||||
};
|
||||
78
webapp/components/get_post_link_modal.jsx
Обычный файл
78
webapp/components/get_post_link_modal.jsx
Обычный файл
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import GetLinkModal from './get_link_modal.jsx';
|
||||
import ModalStore from 'stores/modal_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
title: {
|
||||
id: 'get_post_link_modal.title',
|
||||
defaultMessage: 'Copy Permalink'
|
||||
},
|
||||
help: {
|
||||
id: 'get_post_link_modal.help',
|
||||
defaultMessage: 'The link below allows authorized users to see your post.'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class GetPostLinkModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleToggle = this.handleToggle.bind(this);
|
||||
|
||||
this.hide = this.hide.bind(this);
|
||||
|
||||
this.state = {
|
||||
show: false,
|
||||
post: {}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ModalStore.addModalListener(Constants.ActionTypes.TOGGLE_GET_POST_LINK_MODAL, this.handleToggle);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ModalStore.removeModalListener(Constants.ActionTypes.TOGGLE_GET_POST_LINK_MODAL, this.handleToggle);
|
||||
}
|
||||
|
||||
handleToggle(value, args) {
|
||||
this.setState({
|
||||
show: value,
|
||||
post: args.post
|
||||
});
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.setState({
|
||||
show: false
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
return (
|
||||
<GetLinkModal
|
||||
show={this.state.show}
|
||||
onHide={this.hide}
|
||||
title={formatMessage(holders.title)}
|
||||
helpText={formatMessage(holders.help)}
|
||||
link={TeamStore.getCurrentTeamUrl() + '/pl/' + this.state.post.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GetPostLinkModal.propTypes = {
|
||||
intl: intlShape.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(GetPostLinkModal);
|
||||
78
webapp/components/get_team_invite_link_modal.jsx
Обычный файл
78
webapp/components/get_team_invite_link_modal.jsx
Обычный файл
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import GetLinkModal from './get_link_modal.jsx';
|
||||
import ModalStore from 'stores/modal_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
title: {
|
||||
id: 'get_team_invite_link_modal.title',
|
||||
defaultMessage: 'Team Invite Link'
|
||||
},
|
||||
help: {
|
||||
id: 'get_team_invite_link_modal.help',
|
||||
defaultMessage: 'Send teammates the link below for them to sign-up to this team site. The Team Invite Link can be shared with multiple teammates as it does not change unless it\'s regenerated in Team Settings by a Team Admin.'
|
||||
},
|
||||
helpDisabled: {
|
||||
id: 'get_team_invite_link_modal.helpDisabled',
|
||||
defaultMessage: 'User creation has been disabled for your team. Please ask your team administrator for details.'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class GetTeamInviteLinkModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleToggle = this.handleToggle.bind(this);
|
||||
|
||||
this.state = {
|
||||
show: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ModalStore.addModalListener(Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, this.handleToggle);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ModalStore.removeModalListener(Constants.ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL, this.handleToggle);
|
||||
}
|
||||
|
||||
handleToggle(value) {
|
||||
this.setState({
|
||||
show: value
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
let helpText = formatMessage(holders.helpDisabled);
|
||||
|
||||
if (global.window.mm_config.EnableUserCreation === 'true') {
|
||||
helpText = formatMessage(holders.help);
|
||||
}
|
||||
|
||||
return (
|
||||
<GetLinkModal
|
||||
show={this.state.show}
|
||||
onHide={() => this.setState({show: false})}
|
||||
title={formatMessage(holders.title)}
|
||||
helpText={helpText}
|
||||
link={TeamStore.getCurrentInviteLink()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
GetTeamInviteLinkModal.propTypes = {
|
||||
intl: intlShape.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(GetTeamInviteLinkModal);
|
||||
516
webapp/components/invite_member_modal.jsx
Обычный файл
516
webapp/components/invite_member_modal.jsx
Обычный файл
@@ -0,0 +1,516 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as utils from 'utils/utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
import ModalStore from 'stores/modal_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
import ConfirmModal from './confirm_modal.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
const holders = defineMessages({
|
||||
emailError: {
|
||||
id: 'invite_member.emailError',
|
||||
defaultMessage: 'Please enter a valid email address'
|
||||
},
|
||||
firstname: {
|
||||
id: 'invite_member.firstname',
|
||||
defaultMessage: 'First name'
|
||||
},
|
||||
lastname: {
|
||||
id: 'invite_member.lastname',
|
||||
defaultMessage: 'Last name'
|
||||
},
|
||||
modalTitle: {
|
||||
id: 'invite_member.modalTitle',
|
||||
defaultMessage: 'Discard Invitations?'
|
||||
},
|
||||
modalMessage: {
|
||||
id: 'invite_member.modalMessage',
|
||||
defaultMessage: 'You have unsent invitations, are you sure you want to discard them?'
|
||||
},
|
||||
modalButton: {
|
||||
id: 'invite_member.modalButton',
|
||||
defaultMessage: 'Yes, Discard'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class InviteMemberModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleToggle = this.handleToggle.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleHide = this.handleHide.bind(this);
|
||||
this.addInviteFields = this.addInviteFields.bind(this);
|
||||
this.clearFields = this.clearFields.bind(this);
|
||||
this.removeInviteFields = this.removeInviteFields.bind(this);
|
||||
this.showGetTeamInviteLinkModal = this.showGetTeamInviteLinkModal.bind(this);
|
||||
|
||||
this.state = {
|
||||
show: false,
|
||||
inviteIds: [0],
|
||||
idCount: 0,
|
||||
emailErrors: {},
|
||||
firstNameErrors: {},
|
||||
lastNameErrors: {},
|
||||
emailEnabled: global.window.mm_config.SendEmailNotifications === 'true',
|
||||
userCreationEnabled: global.window.mm_config.EnableUserCreation === 'true',
|
||||
showConfirmModal: false,
|
||||
isSendingEmails: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ModalStore.addModalListener(ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, this.handleToggle);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ModalStore.removeModalListener(ActionTypes.TOGGLE_INVITE_MEMBER_MODAL, this.handleToggle);
|
||||
}
|
||||
|
||||
handleToggle(value) {
|
||||
this.setState({
|
||||
show: value
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
if (!this.state.emailEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
var inviteIds = this.state.inviteIds;
|
||||
var count = inviteIds.length;
|
||||
var invites = [];
|
||||
var emailErrors = this.state.emailErrors;
|
||||
var firstNameErrors = this.state.firstNameErrors;
|
||||
var lastNameErrors = this.state.lastNameErrors;
|
||||
var valid = true;
|
||||
|
||||
for (var i = 0; i < count; i++) {
|
||||
var index = inviteIds[i];
|
||||
var invite = {};
|
||||
invite.email = ReactDOM.findDOMNode(this.refs['email' + index]).value.trim();
|
||||
if (!invite.email || !utils.isEmail(invite.email)) {
|
||||
emailErrors[index] = this.props.intl.formatMessage(holders.emailError);
|
||||
valid = false;
|
||||
} else {
|
||||
emailErrors[index] = '';
|
||||
}
|
||||
|
||||
invite.firstName = ReactDOM.findDOMNode(this.refs['first_name' + index]).value.trim();
|
||||
|
||||
invite.lastName = ReactDOM.findDOMNode(this.refs['last_name' + index]).value.trim();
|
||||
|
||||
invites.push(invite);
|
||||
}
|
||||
|
||||
this.setState({emailErrors: emailErrors, firstNameErrors: firstNameErrors, lastNameErrors: lastNameErrors});
|
||||
|
||||
if (!valid || invites.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var data = {};
|
||||
data.invites = invites;
|
||||
|
||||
this.setState({isSendingEmails: true});
|
||||
|
||||
Client.inviteMembers(
|
||||
data,
|
||||
() => {
|
||||
this.handleHide(false);
|
||||
this.setState({isSendingEmails: false});
|
||||
},
|
||||
(err) => {
|
||||
if (err.id === 'api.team.invite_members.already.app_error') {
|
||||
emailErrors[err.detailed_error] = err.message;
|
||||
this.setState({emailErrors: emailErrors});
|
||||
} else {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
|
||||
this.setState({isSendingEmails: false});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleHide(requireConfirm) {
|
||||
if (requireConfirm) {
|
||||
var notEmpty = false;
|
||||
for (var i = 0; i < this.state.inviteIds.length; i++) {
|
||||
var index = this.state.inviteIds[i];
|
||||
if (ReactDOM.findDOMNode(this.refs['email' + index]).value.trim() !== '') {
|
||||
notEmpty = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (notEmpty) {
|
||||
this.setState({
|
||||
showConfirmModal: true
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
this.clearFields();
|
||||
|
||||
this.setState({
|
||||
show: false,
|
||||
showConfirmModal: false
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (!prevState.show && this.state.show) {
|
||||
$(ReactDOM.findDOMNode(this.refs.modalBody)).css('max-height', $(window).height() - 200);
|
||||
}
|
||||
}
|
||||
|
||||
addInviteFields() {
|
||||
var count = this.state.idCount + 1;
|
||||
var inviteIds = this.state.inviteIds;
|
||||
inviteIds.push(count);
|
||||
this.setState({inviteIds: inviteIds, idCount: count});
|
||||
}
|
||||
|
||||
clearFields() {
|
||||
var inviteIds = this.state.inviteIds;
|
||||
|
||||
for (var i = 0; i < inviteIds.length; i++) {
|
||||
var index = inviteIds[i];
|
||||
ReactDOM.findDOMNode(this.refs['email' + index]).value = '';
|
||||
ReactDOM.findDOMNode(this.refs['first_name' + index]).value = '';
|
||||
ReactDOM.findDOMNode(this.refs['last_name' + index]).value = '';
|
||||
}
|
||||
|
||||
this.setState({
|
||||
inviteIds: [0],
|
||||
idCount: 0,
|
||||
emailErrors: {},
|
||||
firstNameErrors: {},
|
||||
lastNameErrors: {}
|
||||
});
|
||||
}
|
||||
|
||||
removeInviteFields(index) {
|
||||
var count = this.state.idCount;
|
||||
var inviteIds = this.state.inviteIds;
|
||||
var i = inviteIds.indexOf(index);
|
||||
if (i > -1) {
|
||||
inviteIds.splice(i, 1);
|
||||
}
|
||||
if (!inviteIds.length) {
|
||||
inviteIds.push(++count);
|
||||
}
|
||||
this.setState({inviteIds: inviteIds, idCount: count});
|
||||
}
|
||||
|
||||
showGetTeamInviteLinkModal() {
|
||||
this.handleHide(false);
|
||||
|
||||
GlobalActions.showGetTeamInviteLinkModal();
|
||||
}
|
||||
|
||||
render() {
|
||||
var currentUser = UserStore.getCurrentUser();
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
if (currentUser != null) {
|
||||
var inviteSections = [];
|
||||
var inviteIds = this.state.inviteIds;
|
||||
for (var i = 0; i < inviteIds.length; i++) {
|
||||
var index = inviteIds[i];
|
||||
var emailError = null;
|
||||
if (this.state.emailErrors[index]) {
|
||||
emailError = <label className='control-label'>{this.state.emailErrors[index]}</label>;
|
||||
}
|
||||
var firstNameError = null;
|
||||
if (this.state.firstNameErrors[index]) {
|
||||
firstNameError = <label className='control-label'>{this.state.firstNameErrors[index]}</label>;
|
||||
}
|
||||
var lastNameError = null;
|
||||
if (this.state.lastNameErrors[index]) {
|
||||
lastNameError = <label className='control-label'>{this.state.lastNameErrors[index]}</label>;
|
||||
}
|
||||
|
||||
var removeButton = null;
|
||||
if (index) {
|
||||
removeButton = (<div>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-link remove__member'
|
||||
onClick={this.removeInviteFields.bind(this, index)}
|
||||
>
|
||||
<span className='fa fa-trash'></span>
|
||||
</button>
|
||||
</div>);
|
||||
}
|
||||
var emailClass = 'form-group invite';
|
||||
if (emailError) {
|
||||
emailClass += ' has-error';
|
||||
}
|
||||
|
||||
var nameFields = null;
|
||||
|
||||
var firstNameClass = 'form-group';
|
||||
if (firstNameError) {
|
||||
firstNameClass += ' has-error';
|
||||
}
|
||||
var lastNameClass = 'form-group';
|
||||
if (lastNameError) {
|
||||
lastNameClass += ' has-error';
|
||||
}
|
||||
nameFields = (<div className='row--invite'>
|
||||
<div className='col-sm-6'>
|
||||
<div className={firstNameClass}>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
ref={'first_name' + index}
|
||||
placeholder={formatMessage(holders.firstname)}
|
||||
maxLength='64'
|
||||
disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
|
||||
spellCheck='false'
|
||||
/>
|
||||
{firstNameError}
|
||||
</div>
|
||||
</div>
|
||||
<div className='col-sm-6'>
|
||||
<div className={lastNameClass}>
|
||||
<input
|
||||
type='text'
|
||||
className='form-control'
|
||||
ref={'last_name' + index}
|
||||
placeholder={formatMessage(holders.lastname)}
|
||||
maxLength='64'
|
||||
disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
|
||||
spellCheck='false'
|
||||
/>
|
||||
{lastNameError}
|
||||
</div>
|
||||
</div>
|
||||
</div>);
|
||||
|
||||
inviteSections[index] = (
|
||||
<div key={'key' + index}>
|
||||
{removeButton}
|
||||
<div className={emailClass}>
|
||||
<input
|
||||
onKeyUp={this.displayNameKeyUp}
|
||||
type='text'
|
||||
ref={'email' + index}
|
||||
className='form-control'
|
||||
placeholder='email@domain.com'
|
||||
maxLength='64'
|
||||
disabled={!this.state.emailEnabled || !this.state.userCreationEnabled}
|
||||
spellCheck='false'
|
||||
/>
|
||||
{emailError}
|
||||
</div>
|
||||
{nameFields}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var serverError = null;
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var content = null;
|
||||
var sendButton = null;
|
||||
|
||||
var defaultChannelName = '';
|
||||
if (ChannelStore.getByName(Constants.DEFAULT_CHANNEL)) {
|
||||
defaultChannelName = ChannelStore.getByName(Constants.DEFAULT_CHANNEL).display_name;
|
||||
}
|
||||
|
||||
if (this.state.emailEnabled && this.state.userCreationEnabled) {
|
||||
content = (
|
||||
<div>
|
||||
{serverError}
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.addInviteFields}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='invite_member.addAnother'
|
||||
defaultMessage='Add another'
|
||||
/>
|
||||
</button>
|
||||
<br/>
|
||||
<br/>
|
||||
<span>
|
||||
<FormattedHTMLMessage
|
||||
id='invite_member.autoJoin'
|
||||
defaultMessage='People invited automatically join the <strong>{channel}</strong> channel.'
|
||||
values={{
|
||||
channel: defaultChannelName
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
var sendButtonLabel = (
|
||||
<FormattedMessage
|
||||
id='invite_member.send'
|
||||
defaultMessage='Send Invitation'
|
||||
/>
|
||||
);
|
||||
if (this.state.isSendingEmails) {
|
||||
sendButtonLabel = (
|
||||
<span><i className='fa fa-spinner fa-spin'/>
|
||||
<FormattedMessage
|
||||
id='invite_member.sending'
|
||||
defaultMessage=' Sending'
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
} else if (this.state.inviteIds.length > 1) {
|
||||
sendButtonLabel = (
|
||||
<FormattedMessage
|
||||
id='invite_member.send2'
|
||||
defaultMessage='Send Invitations'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
sendButton = (
|
||||
<button
|
||||
onClick={this.handleSubmit}
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
disabled={this.state.isSendingEmails}
|
||||
>
|
||||
{sendButtonLabel}
|
||||
</button>
|
||||
);
|
||||
} else if (this.state.userCreationEnabled) {
|
||||
var teamInviteLink = null;
|
||||
if (currentUser && TeamStore.getCurrent().type === 'O') {
|
||||
var link = (
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.showGetTeamInviteLinkModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='invite_member.inviteLink'
|
||||
defaultMessage='Team Invite Link'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
|
||||
teamInviteLink = (
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='invite_member.teamInviteLink'
|
||||
defaultMessage='You can also invite people using the {link}.'
|
||||
values={{
|
||||
link: (link)
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
content = (
|
||||
<div>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='invite_member.content'
|
||||
defaultMessage='Email is currently disabled for your team, and email invitations cannot be sent. Contact your system administrator to enable email and email invitations.'
|
||||
/>
|
||||
</p>
|
||||
{teamInviteLink}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
content = (
|
||||
<div>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='invite_member.disabled'
|
||||
defaultMessage='User creation has been disabled for your team. Please ask your team administrator for details.'
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Modal
|
||||
dialogClassName='modal-invite-member'
|
||||
show={this.state.show}
|
||||
onHide={this.handleHide.bind(this, true)}
|
||||
enforceFocus={!this.state.showConfirmModal}
|
||||
backdrop={this.state.isSendingEmails ? 'static' : true}
|
||||
>
|
||||
<Modal.Header closeButton={!this.state.isSendingEmails}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='invite_member.newMember'
|
||||
defaultMessage='Invite New Member'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body ref='modalBody'>
|
||||
<form role='form'>
|
||||
{inviteSections}
|
||||
</form>
|
||||
{content}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.handleHide.bind(this, true)}
|
||||
disabled={this.state.isSendingEmails}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='invite_member.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
{sendButton}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
<ConfirmModal
|
||||
title={formatMessage(holders.modalTitle)}
|
||||
message={formatMessage(holders.modalMessage)}
|
||||
confirmButton={formatMessage(holders.modalButton)}
|
||||
show={this.state.showConfirmModal}
|
||||
onConfirm={this.handleHide.bind(this, false)}
|
||||
onCancel={() => this.setState({showConfirmModal: false})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
InviteMemberModal.propTypes = {
|
||||
intl: intlShape.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(InviteMemberModal);
|
||||
40
webapp/components/loading_screen.jsx
Обычный файл
40
webapp/components/loading_screen.jsx
Обычный файл
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class LoadingScreen extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className='loading-screen'
|
||||
style={{position: this.props.position}}
|
||||
>
|
||||
<div className='loading__content'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='loading_screen.loading'
|
||||
defaultMessage='Loading'
|
||||
/>
|
||||
</h3>
|
||||
<div className='round round-1'></div>
|
||||
<div className='round round-2'></div>
|
||||
<div className='round round-3'></div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoadingScreen.defaultProps = {
|
||||
position: 'relative'
|
||||
};
|
||||
LoadingScreen.propTypes = {
|
||||
position: React.PropTypes.oneOf(['absolute', 'fixed', 'relative', 'static', 'inherit'])
|
||||
};
|
||||
227
webapp/components/logged_in.jsx
Обычный файл
227
webapp/components/logged_in.jsx
Обычный файл
@@ -0,0 +1,227 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import SocketStore from 'stores/socket_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import ErrorBar from 'components/error_bar.jsx';
|
||||
|
||||
import {browserHistory} from 'react-router';
|
||||
|
||||
import SidebarRight from 'components/sidebar_right.jsx';
|
||||
import SidebarRightMenu from 'components/sidebar_right_menu.jsx';
|
||||
|
||||
// Modals
|
||||
import GetPostLinkModal from 'components/get_post_link_modal.jsx';
|
||||
import GetTeamInviteLinkModal from 'components/get_team_invite_link_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 TeamSettingsModal from 'components/team_settings_modal.jsx';
|
||||
import RemovedFromChannelModal from 'components/removed_from_channel_modal.jsx';
|
||||
import RegisterAppModal from 'components/register_app_modal.jsx';
|
||||
import ImportThemeModal from 'components/user_settings/import_theme_modal.jsx';
|
||||
import InviteMemberModal from 'components/invite_member_modal.jsx';
|
||||
import SelectTeamModal from 'components/admin_console/select_team_modal.jsx';
|
||||
|
||||
const CLIENT_STATUS_INTERVAL = 30000;
|
||||
const BACKSPACE_CHAR = 8;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class LoggedIn extends React.Component {
|
||||
constructor(params) {
|
||||
super(params);
|
||||
|
||||
this.onUserChanged = this.onUserChanged.bind(this);
|
||||
}
|
||||
onUserChanged() {
|
||||
// Grab the current user
|
||||
const user = UserStore.getCurrentUser();
|
||||
|
||||
// Update segment indentify
|
||||
if (global.window.mm_config.SegmentDeveloperKey != null && global.window.mm_config.SegmentDeveloperKey !== '') {
|
||||
global.window.analytics.identify(user.id, {
|
||||
name: user.nickname,
|
||||
email: user.email,
|
||||
createdAt: user.create_at,
|
||||
username: user.username,
|
||||
team_id: user.team_id,
|
||||
id: user.id
|
||||
});
|
||||
}
|
||||
|
||||
// Update CSS classes to match user theme
|
||||
if (user) {
|
||||
if ($.isPlainObject(user.theme_props) && !$.isEmptyObject(user.theme_props)) {
|
||||
Utils.applyTheme(user.theme_props);
|
||||
} else {
|
||||
Utils.applyTheme(Constants.THEMES.default);
|
||||
}
|
||||
}
|
||||
}
|
||||
onSocketChange(msg) {
|
||||
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
|
||||
UserStore.setStatus(msg.user_id, 'online');
|
||||
}
|
||||
}
|
||||
componentWillMount() {
|
||||
// Emit view action
|
||||
GlobalActions.viewLoggedIn();
|
||||
|
||||
// Listen for user
|
||||
UserStore.addChangeListener(this.onUserChanged);
|
||||
|
||||
// Add listner for socker store
|
||||
SocketStore.addChangeListener(this.onSocketChange);
|
||||
|
||||
// Get all statuses regularally. (Soon to be switched to websocket)
|
||||
this.intervalId = setInterval(() => AsyncClient.getStatuses(), CLIENT_STATUS_INTERVAL);
|
||||
|
||||
// Force logout of all tabs if one tab is logged out
|
||||
$(window).bind('storage', (e) => {
|
||||
// when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
|
||||
if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
|
||||
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
|
||||
if (window.BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('detected logout from a different tab'); //eslint-disable-line no-console
|
||||
browserHistory.push('/' + this.props.params.team);
|
||||
}
|
||||
|
||||
if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
|
||||
// make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
|
||||
if (window.BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('detected login from a different tab'); //eslint-disable-line no-console
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
// Because current CSS requires the root tag to have specific stuff
|
||||
$('#root').attr('class', 'channel-view');
|
||||
|
||||
// ???
|
||||
$('body').on('mouseenter mouseleave', '.post', function mouseOver(ev) {
|
||||
if (ev.type === 'mouseenter') {
|
||||
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--after');
|
||||
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--before');
|
||||
} else {
|
||||
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--after');
|
||||
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--before');
|
||||
}
|
||||
});
|
||||
|
||||
$('body').on('mouseenter mouseleave', '.search-item__container .post', function mouseOver(ev) {
|
||||
if (ev.type === 'mouseenter') {
|
||||
$(this).closest('.search-item__container').find('.date-separator').addClass('hovered--after');
|
||||
$(this).closest('.search-item__container').next('div').find('.date-separator').addClass('hovered--before');
|
||||
} else {
|
||||
$(this).closest('.search-item__container').find('.date-separator').removeClass('hovered--after');
|
||||
$(this).closest('.search-item__container').next('div').find('.date-separator').removeClass('hovered--before');
|
||||
}
|
||||
});
|
||||
|
||||
$('body').on('mouseenter mouseleave', '.post.post--comment.same--root', function mouseOver(ev) {
|
||||
if (ev.type === 'mouseenter') {
|
||||
$(this).parent('div').prev('.date-separator, .new-separator').addClass('hovered--comment');
|
||||
$(this).parent('div').next('.date-separator, .new-separator').addClass('hovered--comment');
|
||||
} else {
|
||||
$(this).parent('div').prev('.date-separator, .new-separator').removeClass('hovered--comment');
|
||||
$(this).parent('div').next('.date-separator, .new-separator').removeClass('hovered--comment');
|
||||
}
|
||||
});
|
||||
|
||||
// Device tracking setup
|
||||
var iOS = (/(iPad|iPhone|iPod)/g).test(navigator.userAgent);
|
||||
if (iOS) {
|
||||
$('body').addClass('ios');
|
||||
}
|
||||
|
||||
// Set up tracking for whether the window is active
|
||||
window.isActive = true;
|
||||
$(window).on('focus', () => {
|
||||
AsyncClient.updateLastViewedAt();
|
||||
ChannelStore.resetCounts(ChannelStore.getCurrentId());
|
||||
ChannelStore.emitChange();
|
||||
window.isActive = true;
|
||||
});
|
||||
$(window).on('blur', () => {
|
||||
window.isActive = false;
|
||||
});
|
||||
|
||||
// if preferences have already been stored in local storage do not wait until preference store change is fired and handled in channel.jsx
|
||||
const selectedFont = PreferenceStore.get(Constants.Preferences.CATEGORY_DISPLAY_SETTINGS, 'selected_font', Constants.DEFAULT_FONT);
|
||||
Utils.applyFont(selectedFont);
|
||||
|
||||
// Pervent backspace from navigating back a page
|
||||
$(window).on('keydown.preventBackspace', (e) => {
|
||||
if (e.which === BACKSPACE_CHAR && !$(e.target).is('input, textarea')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
}
|
||||
componentWillUnmount() {
|
||||
$('#root').attr('class', '');
|
||||
clearInterval(this.intervalId);
|
||||
|
||||
$(window).off('focus');
|
||||
$(window).off('blur');
|
||||
|
||||
SocketStore.removeChangeListener(this.onSocketChange);
|
||||
UserStore.removeChangeListener(this.onUserChanged);
|
||||
|
||||
$('body').off('click.userpopover');
|
||||
$('body').off('mouseenter mouseleave', '.post');
|
||||
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
|
||||
|
||||
$('.modal').off('show.bs.modal');
|
||||
|
||||
$(window).off('keydown.preventBackspace');
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className='channel-view'>
|
||||
<ErrorBar/>
|
||||
<div className='container-fluid'>
|
||||
<SidebarRight/>
|
||||
<SidebarRightMenu/>
|
||||
{this.props.sidebar}
|
||||
{this.props.center}
|
||||
|
||||
<GetPostLinkModal/>
|
||||
<GetTeamInviteLinkModal/>
|
||||
<InviteMemberModal/>
|
||||
<ImportThemeModal/>
|
||||
<TeamSettingsModal/>
|
||||
<MoreChannelsModal/>
|
||||
<EditPostModal/>
|
||||
<DeletePostModal/>
|
||||
<RemovedFromChannelModal/>
|
||||
<RegisterAppModal/>
|
||||
<SelectTeamModal/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoggedIn.defaultProps = {
|
||||
};
|
||||
|
||||
LoggedIn.propTypes = {
|
||||
children: React.PropTypes.object,
|
||||
sidebar: React.PropTypes.object,
|
||||
center: React.PropTypes.object,
|
||||
params: React.PropTypes.object
|
||||
};
|
||||
304
webapp/components/login.jsx
Обычный файл
304
webapp/components/login.jsx
Обычный файл
@@ -0,0 +1,304 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import LoginEmail from './login_email.jsx';
|
||||
import LoginUsername from './login_username.jsx';
|
||||
import LoginLdap from './login_ldap.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {browserHistory} from 'react-router';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class Login extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getStateFromStores = this.getStateFromStores.bind(this);
|
||||
this.onTeamChange = this.onTeamChange.bind(this);
|
||||
|
||||
this.state = this.getStateFromStores();
|
||||
}
|
||||
componentDidMount() {
|
||||
TeamStore.addChangeListener(this.onTeamChange);
|
||||
Client.getMeLoggedIn((data) => {
|
||||
if (data && data.logged_in !== 'false') {
|
||||
browserHistory.push('/' + this.props.params.team + '/channels/town-square');
|
||||
}
|
||||
});
|
||||
}
|
||||
componentWillUnmount() {
|
||||
TeamStore.removeChangeListener(this.onTeamChange);
|
||||
}
|
||||
getStateFromStores() {
|
||||
return {
|
||||
currentTeam: TeamStore.getByName(this.props.params.team)
|
||||
};
|
||||
}
|
||||
onTeamChange() {
|
||||
this.setState(this.getStateFromStores());
|
||||
}
|
||||
render() {
|
||||
const currentTeam = this.state.currentTeam;
|
||||
if (currentTeam == null) {
|
||||
return <div/>;
|
||||
}
|
||||
|
||||
const teamDisplayName = currentTeam.display_name;
|
||||
const teamName = currentTeam.name;
|
||||
const ldapEnabled = global.window.mm_config.EnableLdap === 'true';
|
||||
const usernameSigninEnabled = global.window.mm_config.EnableSignInWithUsername === 'true';
|
||||
|
||||
let loginMessage = [];
|
||||
if (global.window.mm_config.EnableSignUpWithGitLab === 'true') {
|
||||
loginMessage.push(
|
||||
<a
|
||||
className='btn btn-custom-login gitlab'
|
||||
key='gitlab'
|
||||
href={'/api/v1/oauth/gitlab/login?team=' + encodeURIComponent(teamName)}
|
||||
>
|
||||
<span className='icon'/>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='login.gitlab'
|
||||
defaultMessage='with GitLab'
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (global.window.mm_config.EnableSignUpWithGoogle === 'true') {
|
||||
loginMessage.push(
|
||||
<a
|
||||
className='btn btn-custom-login google'
|
||||
key='google'
|
||||
href={'/api/v1/oauth/google/login?team=' + encodeURIComponent(teamName)}
|
||||
>
|
||||
<span className='icon'/>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='login.google'
|
||||
defaultMessage='with Google Apps'
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
const extraParam = Utils.getUrlParameter('extra');
|
||||
let extraBox = '';
|
||||
if (extraParam) {
|
||||
if (extraParam === Constants.SIGNIN_CHANGE) {
|
||||
extraBox = (
|
||||
<div className='alert alert-success'>
|
||||
<i className='fa fa-check'/>
|
||||
<FormattedMessage
|
||||
id='login.changed'
|
||||
defaultMessage=' Sign-in method changed successfully'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (extraParam === Constants.SIGNIN_VERIFIED) {
|
||||
extraBox = (
|
||||
<div className='alert alert-success'>
|
||||
<i className='fa fa-check'/>
|
||||
<FormattedMessage
|
||||
id='login.verified'
|
||||
defaultMessage=' Email Verified'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (extraParam === Constants.SESSION_EXPIRED) {
|
||||
extraBox = (
|
||||
<div className='alert alert-warning'>
|
||||
<i className='fa fa-exclamation-triangle'/>
|
||||
<FormattedMessage
|
||||
id='login.session_expired'
|
||||
defaultMessage=' Your session has expired. Please login again.'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let emailSignup;
|
||||
if (global.window.mm_config.EnableSignInWithEmail === 'true') {
|
||||
emailSignup = (
|
||||
<LoginEmail
|
||||
teamName={teamName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (loginMessage.length > 0 && emailSignup) {
|
||||
loginMessage = (
|
||||
<div>
|
||||
{loginMessage}
|
||||
<div className='or__container'>
|
||||
<FormattedMessage
|
||||
id='login.or'
|
||||
defaultMessage='or'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let forgotPassword;
|
||||
if (emailSignup) {
|
||||
forgotPassword = (
|
||||
<div className='form-group'>
|
||||
<a href={'/' + teamName + '/reset_password'}>
|
||||
<FormattedMessage
|
||||
id='login.forgot'
|
||||
defaultMessage='I forgot my password'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let userSignUp = null;
|
||||
if (currentTeam.allow_open_invite) {
|
||||
userSignUp = (
|
||||
<div>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='login.noAccount'
|
||||
defaultMessage="Don't have an account? "
|
||||
/>
|
||||
<a
|
||||
href={'/signup_user_complete/?id=' + currentTeam.invite_id}
|
||||
className='signup-team-login'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='login.create'
|
||||
defaultMessage='Create one now'
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let teamSignUp = null;
|
||||
if (global.window.mm_config.EnableTeamCreation === 'true' && !Utils.isMobileApp()) {
|
||||
teamSignUp = (
|
||||
<div className='margin--extra'>
|
||||
<a
|
||||
href='/'
|
||||
className='signup-team-login'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='login.createTeam'
|
||||
defaultMessage='Create a new team'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let ldapLogin = null;
|
||||
if (global.window.mm_config.EnableLdap === 'true') {
|
||||
ldapLogin = (
|
||||
<LoginLdap
|
||||
teamName={teamName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (ldapEnabled && (loginMessage.length > 0 || emailSignup || usernameSigninEnabled)) {
|
||||
ldapLogin = (
|
||||
<div>
|
||||
<div className='or__container'>
|
||||
<FormattedMessage
|
||||
id='login.or'
|
||||
defaultMessage='or'
|
||||
/>
|
||||
</div>
|
||||
<LoginLdap
|
||||
teamName={teamName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let usernameLogin = null;
|
||||
if (global.window.mm_config.EnableSignInWithUsername === 'true') {
|
||||
usernameLogin = (
|
||||
<LoginUsername
|
||||
teamName={teamName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (usernameSigninEnabled && (loginMessage.length > 0 || emailSignup || ldapEnabled)) {
|
||||
usernameLogin = (
|
||||
<div>
|
||||
<div className='or__container'>
|
||||
<FormattedMessage
|
||||
id='login.or'
|
||||
defaultMessage='or'
|
||||
/>
|
||||
</div>
|
||||
<LoginUsername
|
||||
teamName={teamName}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='signup-header'>
|
||||
<a href='/'>
|
||||
<span className='fa fa-chevron-left'/>
|
||||
<FormattedMessage
|
||||
id='web.header.back'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className='col-sm-12'>
|
||||
<div className='signup-team__container'>
|
||||
<h5 className='margin--less'>
|
||||
<FormattedMessage
|
||||
id='login.signTo'
|
||||
defaultMessage='Sign in to:'
|
||||
/>
|
||||
</h5>
|
||||
<h2 className='signup-team__name'>{teamDisplayName}</h2>
|
||||
<h2 className='signup-team__subdomain'>
|
||||
<FormattedMessage
|
||||
id='login.on'
|
||||
defaultMessage='on {siteName}'
|
||||
values={{
|
||||
siteName: global.window.mm_config.SiteName
|
||||
}}
|
||||
/>
|
||||
</h2>
|
||||
{extraBox}
|
||||
{loginMessage}
|
||||
{emailSignup}
|
||||
{usernameLogin}
|
||||
{ldapLogin}
|
||||
{userSignUp}
|
||||
{forgotPassword}
|
||||
{teamSignUp}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Login.defaultProps = {
|
||||
};
|
||||
Login.propTypes = {
|
||||
params: React.PropTypes.object.isRequired
|
||||
};
|
||||
167
webapp/components/login_email.jsx
Обычный файл
167
webapp/components/login_email.jsx
Обычный файл
@@ -0,0 +1,167 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import {browserHistory} from 'react-router';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
var holders = defineMessages({
|
||||
badTeam: {
|
||||
id: 'login_email.badTeam',
|
||||
defaultMessage: 'Bad team name'
|
||||
},
|
||||
emailReq: {
|
||||
id: 'login_email.emailReq',
|
||||
defaultMessage: 'An email is required'
|
||||
},
|
||||
pwdReq: {
|
||||
id: 'login_email.pwdReq',
|
||||
defaultMessage: 'A password is required'
|
||||
},
|
||||
email: {
|
||||
id: 'login_email.email',
|
||||
defaultMessage: 'Email'
|
||||
},
|
||||
pwd: {
|
||||
id: 'login_email.pwd',
|
||||
defaultMessage: 'Password'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class LoginEmail extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
serverError: ''
|
||||
};
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const {formatMessage} = this.props.intl;
|
||||
var state = {};
|
||||
|
||||
const name = this.props.teamName;
|
||||
if (!name) {
|
||||
state.serverError = formatMessage(holders.badTeam);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const email = this.refs.email.value.trim();
|
||||
if (!email) {
|
||||
state.serverError = formatMessage(holders.emailReq);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const password = this.refs.password.value.trim();
|
||||
if (!password) {
|
||||
state.serverError = formatMessage(holders.pwdReq);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.serverError = '';
|
||||
this.setState(state);
|
||||
|
||||
Client.loginByEmail(name, email, password,
|
||||
() => {
|
||||
UserStore.setLastEmail(email);
|
||||
browserHistory.push('/' + name + '/channels/town-square');
|
||||
},
|
||||
(err) => {
|
||||
if (err.id === 'api.user.login.not_verified.app_error') {
|
||||
window.location.href = '/verify_email?teamname=' + encodeURIComponent(name) + '&email=' + encodeURIComponent(email);
|
||||
return;
|
||||
}
|
||||
state.serverError = err.message;
|
||||
this.valid = false;
|
||||
this.setState(state);
|
||||
}
|
||||
);
|
||||
}
|
||||
render() {
|
||||
let serverError;
|
||||
let errorClass = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <label className='control-label'>{this.state.serverError}</label>;
|
||||
errorClass = ' has-error';
|
||||
}
|
||||
|
||||
let priorEmail = UserStore.getLastEmail();
|
||||
let focusEmail = false;
|
||||
let focusPassword = false;
|
||||
if (priorEmail === '') {
|
||||
focusEmail = true;
|
||||
} else {
|
||||
focusPassword = true;
|
||||
}
|
||||
|
||||
const emailParam = Utils.getUrlParameter('email');
|
||||
if (emailParam) {
|
||||
priorEmail = decodeURIComponent(emailParam);
|
||||
}
|
||||
|
||||
const {formatMessage} = this.props.intl;
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className='signup__email-container'>
|
||||
<div className={'form-group' + errorClass}>
|
||||
{serverError}
|
||||
</div>
|
||||
<div className={'form-group' + errorClass}>
|
||||
<input
|
||||
autoFocus={focusEmail}
|
||||
type='email'
|
||||
className='form-control'
|
||||
name='email'
|
||||
defaultValue={priorEmail}
|
||||
ref='email'
|
||||
placeholder={formatMessage(holders.email)}
|
||||
spellCheck='false'
|
||||
/>
|
||||
</div>
|
||||
<div className={'form-group' + errorClass}>
|
||||
<input
|
||||
autoFocus={focusPassword}
|
||||
type='password'
|
||||
className='form-control'
|
||||
name='password'
|
||||
ref='password'
|
||||
placeholder={formatMessage(holders.pwd)}
|
||||
spellCheck='false'
|
||||
/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='login_email.signin'
|
||||
defaultMessage='Sign in'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
LoginEmail.defaultProps = {
|
||||
};
|
||||
|
||||
LoginEmail.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
teamName: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(LoginEmail);
|
||||
144
webapp/components/login_ldap.jsx
Обычный файл
144
webapp/components/login_ldap.jsx
Обычный файл
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
badTeam: {
|
||||
id: 'login_ldap.badTeam',
|
||||
defaultMessage: 'Bad team name'
|
||||
},
|
||||
idReq: {
|
||||
id: 'login_ldap.idlReq',
|
||||
defaultMessage: 'An LDAP ID is required'
|
||||
},
|
||||
pwdReq: {
|
||||
id: 'login_ldap.pwdReq',
|
||||
defaultMessage: 'An LDAP password is required'
|
||||
},
|
||||
username: {
|
||||
id: 'login_ldap.username',
|
||||
defaultMessage: 'LDAP Username'
|
||||
},
|
||||
pwd: {
|
||||
id: 'login_ldap.pwd',
|
||||
defaultMessage: 'LDAP Password'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class LoginLdap extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
serverError: ''
|
||||
};
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const {formatMessage} = this.props.intl;
|
||||
var state = {};
|
||||
|
||||
const teamName = this.props.teamName;
|
||||
if (!teamName) {
|
||||
state.serverError = formatMessage(holders.badTeam);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const id = this.refs.id.value.trim();
|
||||
if (!id) {
|
||||
state.serverError = formatMessage(holders.idReq);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const password = this.refs.password.value.trim();
|
||||
if (!password) {
|
||||
state.serverError = formatMessage(holders.pwdReq);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.serverError = '';
|
||||
this.setState(state);
|
||||
|
||||
Client.loginByLdap(teamName, id, password,
|
||||
() => {
|
||||
const redirect = Utils.getUrlParameter('redirect');
|
||||
if (redirect) {
|
||||
window.location.href = decodeURIComponent(redirect);
|
||||
} else {
|
||||
window.location.href = '/' + teamName + '/channels/town-square';
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
state.serverError = err.message;
|
||||
this.setState(state);
|
||||
}
|
||||
);
|
||||
}
|
||||
render() {
|
||||
let serverError;
|
||||
let errorClass = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <label className='control-label'>{this.state.serverError}</label>;
|
||||
errorClass = ' has-error';
|
||||
}
|
||||
const {formatMessage} = this.props.intl;
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className='signup__email-container'>
|
||||
<div className={'form-group' + errorClass}>
|
||||
{serverError}
|
||||
</div>
|
||||
<div className={'form-group' + errorClass}>
|
||||
<input
|
||||
autoFocus={true}
|
||||
className='form-control'
|
||||
ref='id'
|
||||
placeholder={formatMessage(holders.username)}
|
||||
spellCheck='false'
|
||||
/>
|
||||
</div>
|
||||
<div className={'form-group' + errorClass}>
|
||||
<input
|
||||
type='password'
|
||||
className='form-control'
|
||||
ref='password'
|
||||
placeholder={formatMessage(holders.pwd)}
|
||||
spellCheck='false'
|
||||
/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='login_ldap.signin'
|
||||
defaultMessage='Sign in'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
LoginLdap.defaultProps = {
|
||||
};
|
||||
|
||||
LoginLdap.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
teamName: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(LoginLdap);
|
||||
183
webapp/components/login_username.jsx
Обычный файл
183
webapp/components/login_username.jsx
Обычный файл
@@ -0,0 +1,183 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import {injectIntl, intlShape, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
var holders = defineMessages({
|
||||
badTeam: {
|
||||
id: 'login_username.badTeam',
|
||||
defaultMessage: 'Bad team name'
|
||||
},
|
||||
usernameReq: {
|
||||
id: 'login_username.usernameReq',
|
||||
defaultMessage: 'A username is required'
|
||||
},
|
||||
pwdReq: {
|
||||
id: 'login_username.pwdReq',
|
||||
defaultMessage: 'A password is required'
|
||||
},
|
||||
verifyEmailError: {
|
||||
id: 'login_username.verifyEmailError',
|
||||
defaultMessage: 'Please verify your email address. Check your inbox for an email.'
|
||||
},
|
||||
userNotFoundError: {
|
||||
id: 'login_username.userNotFoundError',
|
||||
defaultMessage: "We couldn't find an existing account matching your username for this team."
|
||||
},
|
||||
username: {
|
||||
id: 'login_username.username',
|
||||
defaultMessage: 'Username'
|
||||
},
|
||||
pwd: {
|
||||
id: 'login_username.pwd',
|
||||
defaultMessage: 'Password'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class LoginUsername extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.state = {
|
||||
serverError: ''
|
||||
};
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
const {formatMessage} = this.props.intl;
|
||||
var state = {};
|
||||
|
||||
const name = this.props.teamName;
|
||||
if (!name) {
|
||||
state.serverError = formatMessage(holders.badTeam);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const username = this.refs.username.value.trim();
|
||||
if (!username) {
|
||||
state.serverError = formatMessage(holders.usernameReq);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
const password = this.refs.password.value.trim();
|
||||
if (!password) {
|
||||
state.serverError = formatMessage(holders.pwdReq);
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
|
||||
state.serverError = '';
|
||||
this.setState(state);
|
||||
|
||||
Client.loginByUsername(name, username, password,
|
||||
() => {
|
||||
UserStore.setLastUsername(username);
|
||||
|
||||
const redirect = Utils.getUrlParameter('redirect');
|
||||
if (redirect) {
|
||||
window.location.href = decodeURIComponent(redirect);
|
||||
} else {
|
||||
window.location.href = '/' + name + '/channels/town-square';
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
if (err.id === 'api.user.login.not_verified.app_error') {
|
||||
state.serverError = formatMessage(holders.verifyEmailError);
|
||||
} else if (err.id === 'store.sql_user.get_by_username.app_error') {
|
||||
state.serverError = formatMessage(holders.userNotFoundError);
|
||||
} else {
|
||||
state.serverError = err.message;
|
||||
}
|
||||
|
||||
this.valid = false;
|
||||
this.setState(state);
|
||||
}
|
||||
);
|
||||
}
|
||||
render() {
|
||||
let serverError;
|
||||
let errorClass = '';
|
||||
if (this.state.serverError) {
|
||||
serverError = <label className='control-label'>{this.state.serverError}</label>;
|
||||
errorClass = ' has-error';
|
||||
}
|
||||
|
||||
let priorUsername = UserStore.getLastUsername();
|
||||
let focusUsername = false;
|
||||
let focusPassword = false;
|
||||
if (priorUsername === '') {
|
||||
focusUsername = true;
|
||||
} else {
|
||||
focusPassword = true;
|
||||
}
|
||||
|
||||
const emailParam = Utils.getUrlParameter('email');
|
||||
if (emailParam) {
|
||||
priorUsername = decodeURIComponent(emailParam);
|
||||
}
|
||||
|
||||
const {formatMessage} = this.props.intl;
|
||||
return (
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<div className='signup__email-container'>
|
||||
<div className={'form-group' + errorClass}>
|
||||
{serverError}
|
||||
</div>
|
||||
<div className={'form-group' + errorClass}>
|
||||
<input
|
||||
autoFocus={focusUsername}
|
||||
type='username'
|
||||
className='form-control'
|
||||
name='username'
|
||||
defaultValue={priorUsername}
|
||||
ref='username'
|
||||
placeholder={formatMessage(holders.username)}
|
||||
spellCheck='false'
|
||||
/>
|
||||
</div>
|
||||
<div className={'form-group' + errorClass}>
|
||||
<input
|
||||
autoFocus={focusPassword}
|
||||
type='password'
|
||||
className='form-control'
|
||||
name='password'
|
||||
ref='password'
|
||||
placeholder={formatMessage(holders.pwd)}
|
||||
spellCheck='false'
|
||||
/>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='login_username.signin'
|
||||
defaultMessage='Sign in'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
LoginUsername.defaultProps = {
|
||||
};
|
||||
|
||||
LoginUsername.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
teamName: React.PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(LoginUsername);
|
||||
62
webapp/components/member_list_team.jsx
Обычный файл
62
webapp/components/member_list_team.jsx
Обычный файл
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import FilteredUserList from './filtered_user_list.jsx';
|
||||
import TeamMembersDropdown from './team_members_dropdown.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class MemberListTeam extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getUsers = this.getUsers.bind(this);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
users: this.getUsers()
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
UserStore.addChangeListener(this.onChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
UserStore.removeChangeListener(this.onChange);
|
||||
}
|
||||
|
||||
getUsers() {
|
||||
const profiles = UserStore.getProfiles();
|
||||
const users = [];
|
||||
|
||||
for (const id of Object.keys(profiles)) {
|
||||
users.push(profiles[id]);
|
||||
}
|
||||
|
||||
users.sort((a, b) => a.username.localeCompare(b.username));
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
onChange() {
|
||||
this.setState({
|
||||
users: this.getUsers()
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<FilteredUserList
|
||||
style={this.props.style}
|
||||
users={this.state.users}
|
||||
actions={[TeamMembersDropdown]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MemberListTeam.propTypes = {
|
||||
style: React.PropTypes.object
|
||||
};
|
||||
28
webapp/components/message_wrapper.jsx
Обычный файл
28
webapp/components/message_wrapper.jsx
Обычный файл
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class MessageWrapper extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
render() {
|
||||
if (this.props.message) {
|
||||
return <div dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, this.props.options)}}/>;
|
||||
}
|
||||
|
||||
return <div/>;
|
||||
}
|
||||
}
|
||||
|
||||
MessageWrapper.defaultProps = {
|
||||
message: ''
|
||||
};
|
||||
MessageWrapper.propTypes = {
|
||||
message: React.PropTypes.string,
|
||||
options: React.PropTypes.object
|
||||
};
|
||||
233
webapp/components/more_channels.jsx
Обычный файл
233
webapp/components/more_channels.jsx
Обычный файл
@@ -0,0 +1,233 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
import NewChannelFlow from './new_channel_flow.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import loadingGif from 'images/load.gif';
|
||||
|
||||
function getStateFromStores() {
|
||||
return {
|
||||
channels: ChannelStore.getMoreAll(),
|
||||
serverError: null
|
||||
};
|
||||
}
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class MoreChannels extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
this.handleJoin = this.handleJoin.bind(this);
|
||||
this.handleNewChannel = this.handleNewChannel.bind(this);
|
||||
this.createChannelRow = this.createChannelRow.bind(this);
|
||||
|
||||
var initState = getStateFromStores();
|
||||
initState.channelType = '';
|
||||
initState.joiningChannel = -1;
|
||||
initState.showNewChannelModal = false;
|
||||
this.state = initState;
|
||||
}
|
||||
componentDidMount() {
|
||||
ChannelStore.addMoreChangeListener(this.onListenerChange);
|
||||
$(ReactDOM.findDOMNode(this.refs.modal)).on('shown.bs.modal', () => {
|
||||
AsyncClient.getMoreChannels(true);
|
||||
});
|
||||
|
||||
var self = this;
|
||||
$(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', (e) => {
|
||||
var button = e.relatedTarget;
|
||||
self.setState({channelType: $(button).attr('data-channeltype')});
|
||||
});
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeMoreChangeListener(this.onListenerChange);
|
||||
}
|
||||
onListenerChange() {
|
||||
var newState = getStateFromStores();
|
||||
if (!Utils.areObjectsEqual(newState.channels, this.state.channels)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
handleJoin(channel, channelIndex) {
|
||||
this.setState({joiningChannel: channelIndex});
|
||||
client.joinChannel(channel.id,
|
||||
() => {
|
||||
$(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
|
||||
AsyncClient.getChannel(channel.id);
|
||||
Utils.switchChannel(channel);
|
||||
this.setState({joiningChannel: -1});
|
||||
},
|
||||
(err) => {
|
||||
this.setState({joiningChannel: -1, serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
handleNewChannel() {
|
||||
$(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
|
||||
this.setState({showNewChannelModal: true});
|
||||
}
|
||||
createChannelRow(channel, index) {
|
||||
let joinButton;
|
||||
if (this.state.joiningChannel === index) {
|
||||
joinButton = (
|
||||
<img
|
||||
className='join-channel-loading-gif'
|
||||
src={loadingGif}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
joinButton = (
|
||||
<button
|
||||
onClick={this.handleJoin.bind(self, channel, index)}
|
||||
className='btn btn-primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='more_channels.join'
|
||||
defaultMessage='Join'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='more-modal__row'
|
||||
key={channel.id}
|
||||
>
|
||||
<div className='more-modal__details'>
|
||||
<p className='more-modal__name'>{channel.display_name}</p>
|
||||
<p className='more-modal__description'>{channel.purpose}</p>
|
||||
</div>
|
||||
<div className='more-modal__actions'>
|
||||
{joinButton}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
let maxHeight = 1000;
|
||||
if (Utils.windowHeight() <= 1200) {
|
||||
maxHeight = Utils.windowHeight() - 300;
|
||||
}
|
||||
|
||||
var serverError;
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var moreChannels;
|
||||
|
||||
if (this.state.channels != null) {
|
||||
var channels = this.state.channels;
|
||||
if (channels.loading) {
|
||||
moreChannels = <LoadingScreen/>;
|
||||
} else if (channels.length) {
|
||||
moreChannels = (
|
||||
<div className='more-modal__list'>
|
||||
{channels.map(this.createChannelRow)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
moreChannels = (
|
||||
<div className='no-channel-message'>
|
||||
<p className='primary-message'>
|
||||
<FormattedMessage
|
||||
id='more_channels.noMore'
|
||||
defaultMessage='No more channels to join'
|
||||
/>
|
||||
</p>
|
||||
<p className='secondary-message'>
|
||||
<FormattedMessage
|
||||
id='more_channels.createClick'
|
||||
defaultMessage="Click 'Create New Channel' to make a new one"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='modal fade'
|
||||
id='more_channels'
|
||||
ref='modal'
|
||||
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'
|
||||
>
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
<span className='sr-only'>
|
||||
<FormattedMessage
|
||||
id='more_channels.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
<h4 className='modal-title'>
|
||||
<FormattedMessage
|
||||
id='more_channels.title'
|
||||
defaultMessage='More Channels'
|
||||
/>
|
||||
</h4>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary channel-create-btn'
|
||||
onClick={this.handleNewChannel}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='more_channels.create'
|
||||
defaultMessage='Create New Channel'
|
||||
/>
|
||||
</button>
|
||||
<NewChannelFlow
|
||||
show={this.state.showNewChannelModal}
|
||||
channelType={this.state.channelType}
|
||||
onModalDismissed={() => this.setState({showNewChannelModal: false})}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className='modal-body'
|
||||
style={{maxHeight}}
|
||||
>
|
||||
{moreChannels}
|
||||
{serverError}
|
||||
</div>
|
||||
<div className='modal-footer'>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
data-dismiss='modal'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='more_channels.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
156
webapp/components/more_direct_channels.jsx
Обычный файл
156
webapp/components/more_direct_channels.jsx
Обычный файл
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import FilteredUserList from './filtered_user_list.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import loadingGif from 'images/load.gif';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class MoreDirectChannels extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleHide = this.handleHide.bind(this);
|
||||
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
|
||||
this.handleUserChange = this.handleUserChange.bind(this);
|
||||
|
||||
this.createJoinDirectChannelButton = this.createJoinDirectChannelButton.bind(this);
|
||||
|
||||
this.state = {
|
||||
users: this.getUsersFromStore(),
|
||||
loadingDMChannel: -1
|
||||
};
|
||||
}
|
||||
|
||||
getUsersFromStore() {
|
||||
const currentId = UserStore.getCurrentId();
|
||||
const profiles = UserStore.getActiveOnlyProfiles();
|
||||
const users = [];
|
||||
|
||||
for (const id in profiles) {
|
||||
if (id !== currentId) {
|
||||
users.push(profiles[id]);
|
||||
}
|
||||
}
|
||||
|
||||
users.sort((a, b) => a.username.localeCompare(b.username));
|
||||
|
||||
return users;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
UserStore.addChangeListener(this.handleUserChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
UserStore.removeChangeListener(this.handleUserChange);
|
||||
}
|
||||
|
||||
handleHide() {
|
||||
if (this.props.onModalDismissed) {
|
||||
this.props.onModalDismissed();
|
||||
}
|
||||
}
|
||||
|
||||
handleShowDirectChannel(teammate, e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.loadingDMChannel !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({loadingDMChannel: teammate.id});
|
||||
Utils.openDirectChannelToUser(
|
||||
teammate,
|
||||
(channel) => {
|
||||
Utils.switchChannel(channel);
|
||||
this.setState({loadingDMChannel: -1});
|
||||
this.handleHide();
|
||||
},
|
||||
() => {
|
||||
this.setState({loadingDMChannel: -1});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleUserChange() {
|
||||
this.setState({users: this.getUsersFromStore()});
|
||||
}
|
||||
|
||||
createJoinDirectChannelButton({user}) {
|
||||
if (this.state.loadingDMChannel === user.id) {
|
||||
return (
|
||||
<img
|
||||
className='channel-loading-gif'
|
||||
src={loadingGif}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary btn-message'
|
||||
onClick={this.handleShowDirectChannel.bind(this, user)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='more_direct_channels.message'
|
||||
defaultMessage='Message'
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
let maxHeight = 1000;
|
||||
if (Utils.windowHeight() <= 1200) {
|
||||
maxHeight = Utils.windowHeight() - 300;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='more-modal more-direct-channels'
|
||||
show={this.props.show}
|
||||
onHide={this.handleHide}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='more_direct_channels.title'
|
||||
defaultMessage='Direct Messages'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<FilteredUserList
|
||||
style={{maxHeight}}
|
||||
users={this.state.users}
|
||||
actions={[this.createJoinDirectChannelButton]}
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.handleHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='more_direct_channels.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MoreDirectChannels.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onModalDismissed: React.PropTypes.func
|
||||
};
|
||||
137
webapp/components/msg_typing.jsx
Обычный файл
137
webapp/components/msg_typing.jsx
Обычный файл
@@ -0,0 +1,137 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import SocketStore from 'stores/socket_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const SocketEvents = Constants.SocketEvents;
|
||||
|
||||
const holders = defineMessages({
|
||||
someone: {
|
||||
id: 'msg_typing.someone',
|
||||
defaultMessage: 'Someone'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class MsgTyping extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.updateTypingText = this.updateTypingText.bind(this);
|
||||
this.componentWillReceiveProps = this.componentWillReceiveProps.bind(this);
|
||||
|
||||
this.typingUsers = {};
|
||||
this.state = {
|
||||
text: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
SocketStore.addChangeListener(this.onChange);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.channelId !== nextProps.channelId) {
|
||||
for (const u in this.typingUsers) {
|
||||
if (!this.typingUsers.hasOwnProperty(u)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
clearTimeout(this.typingUsers[u]);
|
||||
}
|
||||
this.typingUsers = {};
|
||||
this.setState({text: ''});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
SocketStore.removeChangeListener(this.onChange);
|
||||
}
|
||||
|
||||
onChange(msg) {
|
||||
let username = this.props.intl.formatMessage(holders.someone);
|
||||
if (msg.action === SocketEvents.TYPING &&
|
||||
this.props.channelId === msg.channel_id &&
|
||||
this.props.parentId === msg.props.parent_id) {
|
||||
if (UserStore.hasProfile(msg.user_id)) {
|
||||
username = UserStore.getProfile(msg.user_id).username;
|
||||
}
|
||||
|
||||
if (this.typingUsers[username]) {
|
||||
clearTimeout(this.typingUsers[username]);
|
||||
}
|
||||
|
||||
this.typingUsers[username] = setTimeout(function myTimer(user) {
|
||||
delete this.typingUsers[user];
|
||||
this.updateTypingText();
|
||||
}.bind(this, username), Constants.UPDATE_TYPING_MS);
|
||||
|
||||
this.updateTypingText();
|
||||
} else if (msg.action === SocketEvents.POSTED && msg.channel_id === this.props.channelId) {
|
||||
if (UserStore.hasProfile(msg.user_id)) {
|
||||
username = UserStore.getProfile(msg.user_id).username;
|
||||
}
|
||||
clearTimeout(this.typingUsers[username]);
|
||||
delete this.typingUsers[username];
|
||||
this.updateTypingText();
|
||||
}
|
||||
}
|
||||
|
||||
updateTypingText() {
|
||||
const users = Object.keys(this.typingUsers);
|
||||
let text = '';
|
||||
switch (users.length) {
|
||||
case 0:
|
||||
text = '';
|
||||
break;
|
||||
case 1:
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='msg_typing.isTyping'
|
||||
defaultMessage='{user} is typing...'
|
||||
values={{
|
||||
user: users[0]
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
default: {
|
||||
const last = users.pop();
|
||||
text = (
|
||||
<FormattedMessage
|
||||
id='msg_typing.areTyping'
|
||||
defaultMessage='{users} and {last} are typing...'
|
||||
values={{
|
||||
users: (users.join(', ')),
|
||||
last: (last)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({text});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span className='msg-typing'>{this.state.text}</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MsgTyping.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
channelId: React.PropTypes.string,
|
||||
parentId: React.PropTypes.string
|
||||
};
|
||||
|
||||
export default injectIntl(MsgTyping);
|
||||
569
webapp/components/navbar.jsx
Обычный файл
569
webapp/components/navbar.jsx
Обычный файл
@@ -0,0 +1,569 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import EditChannelHeaderModal from './edit_channel_header_modal.jsx';
|
||||
import EditChannelPurposeModal from './edit_channel_purpose_modal.jsx';
|
||||
import MessageWrapper from './message_wrapper.jsx';
|
||||
import NotifyCounts from './notify_counts.jsx';
|
||||
import ChannelInfoModal from './channel_info_modal.jsx';
|
||||
import ChannelInviteModal from './channel_invite_modal.jsx';
|
||||
import ChannelNotificationsModal from './channel_notifications_modal.jsx';
|
||||
import DeleteChannelModal from './delete_channel_modal.jsx';
|
||||
import RenameChannelModal from './rename_channel_modal.jsx';
|
||||
import ToggleModalButton from './toggle_modal_button.jsx';
|
||||
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
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';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Popover, OverlayTrigger} from 'react-bootstrap';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class Navbar extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChange = this.onChange.bind(this);
|
||||
this.handleLeave = this.handleLeave.bind(this);
|
||||
this.showSearch = this.showSearch.bind(this);
|
||||
|
||||
this.showEditChannelHeaderModal = this.showEditChannelHeaderModal.bind(this);
|
||||
this.showRenameChannelModal = this.showRenameChannelModal.bind(this);
|
||||
this.hideRenameChannelModal = this.hideRenameChannelModal.bind(this);
|
||||
|
||||
this.createCollapseButtons = this.createCollapseButtons.bind(this);
|
||||
this.createDropdown = this.createDropdown.bind(this);
|
||||
|
||||
const state = this.getStateFromStores();
|
||||
state.showEditChannelPurposeModal = false;
|
||||
state.showEditChannelHeaderModal = false;
|
||||
state.showMembersModal = false;
|
||||
state.showRenameChannelModal = false;
|
||||
this.state = state;
|
||||
}
|
||||
getStateFromStores() {
|
||||
return {
|
||||
channel: ChannelStore.getCurrent(),
|
||||
member: ChannelStore.getCurrentMember(),
|
||||
users: ChannelStore.getCurrentExtraInfo().members,
|
||||
currentUser: UserStore.getCurrentUser()
|
||||
};
|
||||
}
|
||||
stateValid() {
|
||||
return this.state.channel && this.state.member && this.state.users && this.state.currentUser;
|
||||
}
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onChange);
|
||||
ChannelStore.addExtraInfoChangeListener(this.onChange);
|
||||
$('.inner-wrap').click(this.hideSidebars);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onChange);
|
||||
ChannelStore.removeExtraInfoChangeListener(this.onChange);
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
}
|
||||
handleLeave() {
|
||||
Client.leaveChannel(this.state.channel.id,
|
||||
() => {
|
||||
AsyncClient.getChannels(true);
|
||||
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
|
||||
},
|
||||
(err) => {
|
||||
AsyncClient.dispatchError(err, 'handleLeave');
|
||||
}
|
||||
);
|
||||
}
|
||||
hideSidebars(e) {
|
||||
var windowWidth = $(window).outerWidth();
|
||||
if (windowWidth <= 768) {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_SEARCH,
|
||||
results: null
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_POST_SELECTED,
|
||||
postId: null
|
||||
});
|
||||
|
||||
if (e.target.className !== 'navbar-toggle' && e.target.className !== 'icon-bar') {
|
||||
$('.inner-wrap').removeClass('move--right move--left move--left-small');
|
||||
$('.sidebar--left').removeClass('move--right');
|
||||
$('.sidebar--right').removeClass('move--left');
|
||||
$('.sidebar--menu').removeClass('move--left');
|
||||
}
|
||||
}
|
||||
}
|
||||
toggleLeftSidebar() {
|
||||
$('.inner-wrap').toggleClass('move--right');
|
||||
$('.sidebar--left').toggleClass('move--right');
|
||||
}
|
||||
toggleRightSidebar() {
|
||||
$('.inner-wrap').toggleClass('move--left-small');
|
||||
$('.sidebar--menu').toggleClass('move--left');
|
||||
}
|
||||
showSearch() {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.SHOW_SEARCH
|
||||
});
|
||||
}
|
||||
onChange() {
|
||||
this.setState(this.getStateFromStores());
|
||||
}
|
||||
showEditChannelHeaderModal() {
|
||||
// this can't be done using a ToggleModalButton because we can't use one inside an OverlayTrigger
|
||||
if (this.refs.headerOverlay) {
|
||||
this.refs.headerOverlay.hide();
|
||||
}
|
||||
|
||||
this.setState({
|
||||
showEditChannelHeaderModal: true
|
||||
});
|
||||
}
|
||||
showRenameChannelModal(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({
|
||||
showRenameChannelModal: true
|
||||
});
|
||||
}
|
||||
hideRenameChannelModal() {
|
||||
this.setState({
|
||||
showRenameChannelModal: false
|
||||
});
|
||||
}
|
||||
createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) {
|
||||
if (channel) {
|
||||
var viewInfoOption = (
|
||||
<li role='presentation'>
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={ChannelInfoModal}
|
||||
dialogProps={{channel}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.viewInfo'
|
||||
defaultMessage='View Info'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</li>
|
||||
);
|
||||
|
||||
var setChannelHeaderOption = (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.showEditChannelHeaderModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.setHeader'
|
||||
defaultMessage='Set Channel Header...'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
||||
var setChannelPurposeOption = null;
|
||||
if (!isDirect) {
|
||||
setChannelPurposeOption = (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={() => this.setState({showEditChannelPurposeModal: true})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.setPurpose'
|
||||
defaultMessage='Set Channel Purpose...'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
var addMembersOption;
|
||||
var leaveChannelOption;
|
||||
if (!isDirect && !ChannelStore.isDefault(channel)) {
|
||||
addMembersOption = (
|
||||
<li role='presentation'>
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={ChannelInviteModal}
|
||||
dialogProps={{channel, currentUser: this.state.currentUser}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.addMembers'
|
||||
defaultMessage='Add Members'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</li>
|
||||
);
|
||||
|
||||
leaveChannelOption = (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.handleLeave}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.leave'
|
||||
defaultMessage='Leave Channel'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
var manageMembersOption;
|
||||
var renameChannelOption;
|
||||
var deleteChannelOption;
|
||||
if (!isDirect && isAdmin) {
|
||||
if (!ChannelStore.isDefault(channel)) {
|
||||
manageMembersOption = (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={() => this.setState({showMembersModal: true})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.manageMembers'
|
||||
defaultMessage='Manage Members'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
||||
deleteChannelOption = (
|
||||
<li role='presentation'>
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={DeleteChannelModal}
|
||||
dialogProps={{channel}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.delete'
|
||||
defaultMessage='Delete Channel...'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
renameChannelOption = (
|
||||
<li role='presentation'>
|
||||
<a
|
||||
role='menuitem'
|
||||
href='#'
|
||||
onClick={this.showRenameChannelModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.rename'
|
||||
defaultMessage='Rename Channel...'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
var notificationPreferenceOption;
|
||||
if (!isDirect) {
|
||||
notificationPreferenceOption = (
|
||||
<li role='presentation'>
|
||||
<ToggleModalButton
|
||||
role='menuitem'
|
||||
dialogType={ChannelNotificationsModal}
|
||||
dialogProps={{
|
||||
channel,
|
||||
channelMember: this.state.member,
|
||||
currentUser: this.state.currentUser
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.preferences'
|
||||
defaultMessage='Notification Preferences'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='navbar-brand'>
|
||||
<div className='dropdown'>
|
||||
<OverlayTrigger
|
||||
ref='headerOverlay'
|
||||
trigger='click'
|
||||
placement='bottom'
|
||||
overlay={popoverContent}
|
||||
className='description'
|
||||
rootClose={true}
|
||||
>
|
||||
<div className='description info-popover'/>
|
||||
</OverlayTrigger>
|
||||
<a
|
||||
href='#'
|
||||
className='dropdown-toggle theme'
|
||||
type='button'
|
||||
data-toggle='dropdown'
|
||||
aria-expanded='true'
|
||||
>
|
||||
<span className='heading'>{channelTitle} </span>
|
||||
<span className='glyphicon glyphicon-chevron-down header-dropdown__icon'></span>
|
||||
</a>
|
||||
<ul
|
||||
className='dropdown-menu'
|
||||
role='menu'
|
||||
>
|
||||
{viewInfoOption}
|
||||
{addMembersOption}
|
||||
{manageMembersOption}
|
||||
{setChannelHeaderOption}
|
||||
{setChannelPurposeOption}
|
||||
{notificationPreferenceOption}
|
||||
{renameChannelOption}
|
||||
{deleteChannelOption}
|
||||
{leaveChannelOption}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='navbar-brand'>
|
||||
<a
|
||||
href={TeamStore.getCurrentTeamUrl() + '/channels/town-square'}
|
||||
className='heading'
|
||||
>
|
||||
{channelTitle}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
createCollapseButtons(currentId) {
|
||||
var buttons = [];
|
||||
if (currentId == null) {
|
||||
buttons.push(
|
||||
<button
|
||||
key='navbar-toggle-collapse'
|
||||
type='button'
|
||||
className='navbar-toggle'
|
||||
data-toggle='collapse'
|
||||
data-target='#navbar-collapse-1'
|
||||
>
|
||||
<span className='sr-only'>
|
||||
<FormattedMessage
|
||||
id='navbar.toggle1'
|
||||
defaultMessage='Toggle sidebar'
|
||||
/>
|
||||
</span>
|
||||
<span className='icon-bar'></span>
|
||||
<span className='icon-bar'></span>
|
||||
<span className='icon-bar'></span>
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
buttons.push(
|
||||
<button
|
||||
key='navbar-toggle-sidebar'
|
||||
type='button'
|
||||
className='navbar-toggle'
|
||||
data-toggle='collapse'
|
||||
data-target='#sidebar-nav'
|
||||
onClick={this.toggleLeftSidebar}
|
||||
>
|
||||
<span className='sr-only'>
|
||||
<FormattedMessage
|
||||
id='navbar.toggle2'
|
||||
defaultMessage='Toggle sidebar'
|
||||
/>
|
||||
</span>
|
||||
<span className='icon-bar'></span>
|
||||
<span className='icon-bar'></span>
|
||||
<span className='icon-bar'></span>
|
||||
<NotifyCounts/>
|
||||
</button>
|
||||
);
|
||||
|
||||
buttons.push(
|
||||
<button
|
||||
key='navbar-toggle-menu'
|
||||
type='button'
|
||||
className='navbar-toggle menu-toggle pull-right'
|
||||
data-toggle='collapse'
|
||||
data-target='#sidebar-nav'
|
||||
onClick={this.toggleRightSidebar}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}}/>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
}
|
||||
render() {
|
||||
if (!this.stateValid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var currentId = this.state.currentUser.id;
|
||||
var channel = this.state.channel;
|
||||
var channelTitle = this.props.teamDisplayName;
|
||||
var popoverContent;
|
||||
var isAdmin = false;
|
||||
var isDirect = false;
|
||||
|
||||
var editChannelHeaderModal = null;
|
||||
var editChannelPurposeModal = null;
|
||||
let renameChannelModal = null;
|
||||
|
||||
if (channel) {
|
||||
popoverContent = (
|
||||
<Popover
|
||||
bsStyle='info'
|
||||
placement='bottom'
|
||||
id='header-popover'
|
||||
>
|
||||
<MessageWrapper
|
||||
message={channel.header}
|
||||
options={{singleline: true, mentionHighlight: false}}
|
||||
/>
|
||||
</Popover>
|
||||
);
|
||||
isAdmin = Utils.isAdmin(this.state.member.roles);
|
||||
|
||||
if (channel.type === 'O') {
|
||||
channelTitle = channel.display_name;
|
||||
} else if (channel.type === 'P') {
|
||||
channelTitle = channel.display_name;
|
||||
} else if (channel.type === 'D') {
|
||||
isDirect = true;
|
||||
if (this.state.users.length > 1) {
|
||||
let p;
|
||||
if (this.state.users[0].id === currentId) {
|
||||
p = UserStore.getProfile(this.state.users[1].id);
|
||||
} else {
|
||||
p = UserStore.getProfile(this.state.users[0].id);
|
||||
}
|
||||
if (p != null) {
|
||||
channelTitle = p.username;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (channel.header.length === 0) {
|
||||
const link = (
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.showEditChannelHeaderModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar.click'
|
||||
defaultMessage='Click here'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
popoverContent = (
|
||||
<Popover
|
||||
bsStyle='info'
|
||||
placement='bottom'
|
||||
id='header-popover'
|
||||
>
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='navbar.noHeader'
|
||||
defaultMessage='No channel header yet.{newline}{link} to add one.'
|
||||
values={{
|
||||
newline: (<br/>),
|
||||
link: (link)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
editChannelHeaderModal = (
|
||||
<EditChannelHeaderModal
|
||||
show={this.state.showEditChannelHeaderModal}
|
||||
onHide={() => this.setState({showEditChannelHeaderModal: false})}
|
||||
channel={channel}
|
||||
/>
|
||||
);
|
||||
|
||||
editChannelPurposeModal = (
|
||||
<EditChannelPurposeModal
|
||||
show={this.state.showEditChannelPurposeModal}
|
||||
onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
|
||||
channel={channel}
|
||||
/>
|
||||
);
|
||||
|
||||
renameChannelModal = (
|
||||
<RenameChannelModal
|
||||
show={this.state.showRenameChannelModal}
|
||||
onHide={this.hideRenameChannelModal}
|
||||
channel={channel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
var collapseButtons = this.createCollapseButtons(currentId);
|
||||
|
||||
const searchButton = (
|
||||
<button
|
||||
type='button'
|
||||
className='navbar-toggle pull-right'
|
||||
onClick={this.showSearch}
|
||||
>
|
||||
<span className='glyphicon glyphicon-search icon--white'/>
|
||||
</button>
|
||||
);
|
||||
|
||||
var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<nav
|
||||
className='navbar navbar-default navbar-fixed-top'
|
||||
role='navigation'
|
||||
>
|
||||
<div className='container-fluid theme'>
|
||||
<div className='navbar-header'>
|
||||
{collapseButtons}
|
||||
{searchButton}
|
||||
{channelMenuDropdown}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
{editChannelHeaderModal}
|
||||
{editChannelPurposeModal}
|
||||
{renameChannelModal}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Navbar.defaultProps = {
|
||||
teamDisplayName: ''
|
||||
};
|
||||
Navbar.propTypes = {
|
||||
teamDisplayName: React.PropTypes.string
|
||||
};
|
||||
281
webapp/components/navbar_dropdown.jsx
Обычный файл
281
webapp/components/navbar_dropdown.jsx
Обычный файл
@@ -0,0 +1,281 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
|
||||
import AboutBuildModal from './about_build_modal.jsx';
|
||||
import TeamMembersModal from './team_members_modal.jsx';
|
||||
import ToggleModalButton from './toggle_modal_button.jsx';
|
||||
import UserSettingsModal from './user_settings/user_settings_modal.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {Link} from 'react-router';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class NavbarDropdown extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.blockToggle = false;
|
||||
|
||||
this.handleAboutModal = this.handleAboutModal.bind(this);
|
||||
this.aboutModalDismissed = this.aboutModalDismissed.bind(this);
|
||||
|
||||
this.state = {
|
||||
showUserSettingsModal: false,
|
||||
showAboutModal: false
|
||||
};
|
||||
}
|
||||
handleAboutModal() {
|
||||
this.setState({showAboutModal: true});
|
||||
}
|
||||
aboutModalDismissed() {
|
||||
this.setState({showAboutModal: false});
|
||||
}
|
||||
componentDidMount() {
|
||||
$(ReactDOM.findDOMNode(this.refs.dropdown)).on('hide.bs.dropdown', () => {
|
||||
$('.sidebar--left .dropdown-menu').scrollTop(0);
|
||||
this.blockToggle = true;
|
||||
setTimeout(() => {
|
||||
this.blockToggle = false;
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
componentWillUnmount() {
|
||||
$(ReactDOM.findDOMNode(this.refs.dropdown)).off('hide.bs.dropdown');
|
||||
}
|
||||
render() {
|
||||
var teamLink = '';
|
||||
var inviteLink = '';
|
||||
var manageLink = '';
|
||||
var sysAdminLink = '';
|
||||
var adminDivider = '';
|
||||
var currentUser = this.props.currentUser;
|
||||
var isAdmin = false;
|
||||
var isSystemAdmin = false;
|
||||
var teamSettings = null;
|
||||
|
||||
if (currentUser != null) {
|
||||
isAdmin = Utils.isAdmin(currentUser.roles);
|
||||
isSystemAdmin = Utils.isSystemAdmin(currentUser.roles);
|
||||
|
||||
inviteLink = (
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
onClick={GlobalActions.showInviteMemberModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.inviteMember'
|
||||
defaultMessage='Invite New Member'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
|
||||
if (this.props.teamType === Constants.OPEN_TEAM && global.window.mm_config.EnableUserCreation === 'true') {
|
||||
teamLink = (
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
onClick={GlobalActions.showGetTeamInviteLinkModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.teamLink'
|
||||
defaultMessage='Get Team Invite Link'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isAdmin) {
|
||||
manageLink = (
|
||||
<li>
|
||||
<ToggleModalButton dialogType={TeamMembersModal}>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.manageMembers'
|
||||
defaultMessage='Manage Members'
|
||||
/>
|
||||
</ToggleModalButton>
|
||||
</li>
|
||||
);
|
||||
|
||||
adminDivider = (<li className='divider'></li>);
|
||||
|
||||
teamSettings = (
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
data-toggle='modal'
|
||||
data-target='#team_settings'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.teamSettings'
|
||||
defaultMessage='Team Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSystemAdmin) {
|
||||
sysAdminLink = (
|
||||
<li>
|
||||
<a
|
||||
href={'/admin_console'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.console'
|
||||
defaultMessage='System Console'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
var teams = [];
|
||||
|
||||
if (global.window.mm_config.EnableTeamCreation === 'true') {
|
||||
teams.push(
|
||||
<li key='newTeam_li'>
|
||||
<a
|
||||
key='newTeam_a'
|
||||
target='_blank'
|
||||
href={Utils.getWindowLocationOrigin() + '/signup_team'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.create'
|
||||
defaultMessage='Create a New Team'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
let helpLink = null;
|
||||
if (global.window.mm_config.HelpLink) {
|
||||
helpLink = (
|
||||
<li>
|
||||
<a
|
||||
target='_blank'
|
||||
href={global.window.mm_config.HelpLink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.help'
|
||||
defaultMessage='Help'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
let reportLink = null;
|
||||
if (global.window.mm_config.ReportAProblemLink) {
|
||||
reportLink = (
|
||||
<li>
|
||||
<a
|
||||
target='_blank'
|
||||
href={global.window.mm_config.ReportAProblemLink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.report'
|
||||
defaultMessage='Report a Problem'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className='nav navbar-nav navbar-right'>
|
||||
<li
|
||||
ref='dropdown'
|
||||
className='dropdown'
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
className='dropdown-toggle'
|
||||
data-toggle='dropdown'
|
||||
role='button'
|
||||
aria-expanded='false'
|
||||
>
|
||||
<span
|
||||
className='dropdown__icon'
|
||||
dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}}
|
||||
/>
|
||||
</a>
|
||||
<ul
|
||||
className='dropdown-menu'
|
||||
role='menu'
|
||||
>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
onClick={() => this.setState({showUserSettingsModal: true})}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.accountSettings'
|
||||
defaultMessage='Account Settings'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
{inviteLink}
|
||||
{teamLink}
|
||||
<li>
|
||||
<Link to={'/' + this.props.teamName + '/logout'}>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.logout'
|
||||
defaultMessage='Logout'
|
||||
/>
|
||||
</Link>
|
||||
</li>
|
||||
{adminDivider}
|
||||
{teamSettings}
|
||||
{manageLink}
|
||||
{sysAdminLink}
|
||||
{teams}
|
||||
<li className='divider'></li>
|
||||
{helpLink}
|
||||
{reportLink}
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.handleAboutModal}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.about'
|
||||
defaultMessage='About Mattermost'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<UserSettingsModal
|
||||
show={this.state.showUserSettingsModal}
|
||||
onModalDismissed={() => this.setState({showUserSettingsModal: false})}
|
||||
/>
|
||||
<AboutBuildModal
|
||||
show={this.state.showAboutModal}
|
||||
onModalDismissed={this.aboutModalDismissed}
|
||||
/>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NavbarDropdown.defaultProps = {
|
||||
teamType: ''
|
||||
};
|
||||
NavbarDropdown.propTypes = {
|
||||
teamType: React.PropTypes.string,
|
||||
teamDisplayName: React.PropTypes.string,
|
||||
teamName: React.PropTypes.string,
|
||||
currentUser: React.PropTypes.object
|
||||
};
|
||||
22
webapp/components/needs_team.jsx
Обычный файл
22
webapp/components/needs_team.jsx
Обычный файл
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class NeedsTeam extends React.Component {
|
||||
componentWillMount() {
|
||||
GlobalActions.loadTeamRequiredPage();
|
||||
}
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
NeedsTeam.defaultProps = {
|
||||
};
|
||||
|
||||
NeedsTeam.propTypes = {
|
||||
children: React.PropTypes.object
|
||||
};
|
||||
250
webapp/components/new_channel_flow.jsx
Обычный файл
250
webapp/components/new_channel_flow.jsx
Обычный файл
@@ -0,0 +1,250 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import NewChannelModal from './new_channel_modal.jsx';
|
||||
import ChangeURLModal from './change_url_modal.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages} from 'react-intl';
|
||||
|
||||
const SHOW_NEW_CHANNEL = 1;
|
||||
const SHOW_EDIT_URL = 2;
|
||||
const SHOW_EDIT_URL_THEN_COMPLETE = 3;
|
||||
const messages = defineMessages({
|
||||
invalidName: {
|
||||
id: 'channel_flow.invalidName',
|
||||
defaultMessage: 'Invalid Channel Name'
|
||||
},
|
||||
alreadyExist: {
|
||||
id: 'channel_flow.alreadyExist',
|
||||
defaultMessage: 'A channel with that URL already exists'
|
||||
},
|
||||
channel: {
|
||||
id: 'channel_flow.channel',
|
||||
defaultMessage: 'Channel'
|
||||
},
|
||||
group: {
|
||||
id: 'channel_flow.group',
|
||||
defaultMessage: 'Group'
|
||||
},
|
||||
change: {
|
||||
id: 'channel_flow.changeUrlTitle',
|
||||
defaultMessage: 'Change {term} URL'
|
||||
},
|
||||
set: {
|
||||
id: 'channel_flow.set_url_title',
|
||||
defaultMessage: 'Set {term} URL'
|
||||
},
|
||||
create: {
|
||||
id: 'channel_flow.create',
|
||||
defaultMessage: 'Create {term}'
|
||||
},
|
||||
changeUrlDescription: {
|
||||
id: 'channel_flow.changeUrlDescription',
|
||||
defaultMessage: 'Some characters are not allowed in URLs and may be removed.'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class NewChannelFlow extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.doSubmit = this.doSubmit.bind(this);
|
||||
this.typeSwitched = this.typeSwitched.bind(this);
|
||||
this.urlChangeRequested = this.urlChangeRequested.bind(this);
|
||||
this.urlChangeSubmitted = this.urlChangeSubmitted.bind(this);
|
||||
this.urlChangeDismissed = this.urlChangeDismissed.bind(this);
|
||||
this.channelDataChanged = this.channelDataChanged.bind(this);
|
||||
|
||||
this.state = {
|
||||
serverError: '',
|
||||
channelType: 'O',
|
||||
flowState: SHOW_NEW_CHANNEL,
|
||||
channelDisplayName: '',
|
||||
channelName: '',
|
||||
channelPurpose: '',
|
||||
nameModified: false
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
// If we are being shown, grab channel type from props and clear
|
||||
if (nextProps.show === true && this.props.show === false) {
|
||||
this.setState({
|
||||
serverError: '',
|
||||
channelType: nextProps.channelType,
|
||||
flowState: SHOW_NEW_CHANNEL,
|
||||
channelDisplayName: '',
|
||||
channelName: '',
|
||||
channelPurpose: '',
|
||||
nameModified: false
|
||||
});
|
||||
}
|
||||
}
|
||||
doSubmit() {
|
||||
var channel = {};
|
||||
|
||||
const {formatMessage} = this.props.intl;
|
||||
channel.display_name = this.state.channelDisplayName;
|
||||
if (!channel.display_name) {
|
||||
this.setState({serverError: formatMessage(messages.invalidName)});
|
||||
return;
|
||||
}
|
||||
|
||||
channel.name = this.state.channelName;
|
||||
if (channel.name.length < 2) {
|
||||
this.setState({flowState: SHOW_EDIT_URL_THEN_COMPLETE});
|
||||
return;
|
||||
}
|
||||
|
||||
const cu = UserStore.getCurrentUser();
|
||||
channel.team_id = cu.team_id;
|
||||
channel.purpose = this.state.channelPurpose;
|
||||
channel.type = this.state.channelType;
|
||||
|
||||
Client.createChannel(channel,
|
||||
(data) => {
|
||||
this.props.onModalDismissed();
|
||||
AsyncClient.getChannel(data.id);
|
||||
Utils.switchChannel(data);
|
||||
},
|
||||
(err) => {
|
||||
if (err.id === 'model.channel.is_valid.2_or_more.app_error') {
|
||||
this.setState({flowState: SHOW_EDIT_URL_THEN_COMPLETE});
|
||||
}
|
||||
if (err.id === 'store.sql_channel.update.exists.app_error') {
|
||||
this.setState({serverError: formatMessage(messages.alreadyExist)});
|
||||
return;
|
||||
}
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
typeSwitched() {
|
||||
if (this.state.channelType === 'P') {
|
||||
this.setState({channelType: 'O'});
|
||||
} else {
|
||||
this.setState({channelType: 'P'});
|
||||
}
|
||||
}
|
||||
urlChangeRequested() {
|
||||
this.setState({flowState: SHOW_EDIT_URL});
|
||||
}
|
||||
urlChangeSubmitted(newURL) {
|
||||
if (this.state.flowState === SHOW_EDIT_URL_THEN_COMPLETE) {
|
||||
this.setState({channelName: newURL, nameModified: true}, this.doSubmit);
|
||||
} else {
|
||||
this.setState({flowState: SHOW_NEW_CHANNEL, serverError: '', channelName: newURL, nameModified: true});
|
||||
}
|
||||
}
|
||||
urlChangeDismissed() {
|
||||
this.setState({flowState: SHOW_NEW_CHANNEL});
|
||||
}
|
||||
channelDataChanged(data) {
|
||||
this.setState({
|
||||
channelDisplayName: data.displayName,
|
||||
channelPurpose: data.purpose
|
||||
});
|
||||
if (!this.state.nameModified) {
|
||||
this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
|
||||
}
|
||||
}
|
||||
render() {
|
||||
const channelData = {
|
||||
name: this.state.channelName,
|
||||
displayName: this.state.channelDisplayName,
|
||||
purpose: this.state.channelPurpose
|
||||
};
|
||||
|
||||
let showChannelModal = false;
|
||||
let showGroupModal = false;
|
||||
let showChangeURLModal = false;
|
||||
|
||||
let changeURLTitle = '';
|
||||
let changeURLSubmitButtonText = '';
|
||||
let channelTerm = '';
|
||||
|
||||
const {formatMessage} = this.props.intl;
|
||||
|
||||
// Only listen to flow state if we are being shown
|
||||
if (this.props.show) {
|
||||
switch (this.state.flowState) {
|
||||
case SHOW_NEW_CHANNEL:
|
||||
if (this.state.channelType === 'O') {
|
||||
showChannelModal = true;
|
||||
channelTerm = formatMessage(messages.channel);
|
||||
} else {
|
||||
showGroupModal = true;
|
||||
channelTerm = formatMessage(messages.group);
|
||||
}
|
||||
break;
|
||||
case SHOW_EDIT_URL:
|
||||
showChangeURLModal = true;
|
||||
changeURLTitle = formatMessage(messages.change, {term: channelTerm});
|
||||
changeURLSubmitButtonText = formatMessage(messages.change, {term: channelTerm});
|
||||
break;
|
||||
case SHOW_EDIT_URL_THEN_COMPLETE:
|
||||
showChangeURLModal = true;
|
||||
changeURLTitle = formatMessage(messages.set, {term: channelTerm});
|
||||
changeURLSubmitButtonText = formatMessage(messages.create, {term: channelTerm});
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<NewChannelModal
|
||||
show={showChannelModal}
|
||||
channelType={'O'}
|
||||
channelData={channelData}
|
||||
serverError={this.state.serverError}
|
||||
onSubmitChannel={this.doSubmit}
|
||||
onModalDismissed={this.props.onModalDismissed}
|
||||
onTypeSwitched={this.typeSwitched}
|
||||
onChangeURLPressed={this.urlChangeRequested}
|
||||
onDataChanged={this.channelDataChanged}
|
||||
/>
|
||||
<NewChannelModal
|
||||
show={showGroupModal}
|
||||
channelType={'P'}
|
||||
channelData={channelData}
|
||||
serverError={this.state.serverError}
|
||||
onSubmitChannel={this.doSubmit}
|
||||
onModalDismissed={this.props.onModalDismissed}
|
||||
onTypeSwitched={this.typeSwitched}
|
||||
onChangeURLPressed={this.urlChangeRequested}
|
||||
onDataChanged={this.channelDataChanged}
|
||||
/>
|
||||
<ChangeURLModal
|
||||
show={showChangeURLModal}
|
||||
title={changeURLTitle}
|
||||
description={formatMessage(messages.changeUrlDescription)}
|
||||
urlLabel={channelTerm + ' URL'}
|
||||
submitButtonText={changeURLSubmitButtonText}
|
||||
currentURL={this.state.channelName}
|
||||
serverError={this.state.serverError}
|
||||
onModalSubmit={this.urlChangeSubmitted}
|
||||
onModalDismissed={this.urlChangeDismissed}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NewChannelFlow.defaultProps = {
|
||||
show: false,
|
||||
channelType: 'O'
|
||||
};
|
||||
|
||||
NewChannelFlow.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
channelType: React.PropTypes.string.isRequired,
|
||||
onModalDismissed: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(NewChannelFlow);
|
||||
288
webapp/components/new_channel_modal.jsx
Обычный файл
288
webapp/components/new_channel_modal.jsx
Обычный файл
@@ -0,0 +1,288 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
const holders = defineMessages({
|
||||
nameEx: {
|
||||
id: 'channel_modal.nameEx',
|
||||
defaultMessage: 'E.g.: "Bugs", "Marketing", "办公室恋情"'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class NewChannelModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
|
||||
this.state = {
|
||||
displayNameError: ''
|
||||
};
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.show === true && this.props.show === false) {
|
||||
this.setState({
|
||||
displayNameError: ''
|
||||
});
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
if (Utils.isBrowserIE()) {
|
||||
$('body').addClass('browser--IE');
|
||||
}
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const displayName = ReactDOM.findDOMNode(this.refs.display_name).value.trim();
|
||||
if (displayName.length < 1) {
|
||||
this.setState({displayNameError: true});
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onSubmitChannel();
|
||||
}
|
||||
handleChange() {
|
||||
const newData = {
|
||||
displayName: ReactDOM.findDOMNode(this.refs.display_name).value,
|
||||
purpose: ReactDOM.findDOMNode(this.refs.channel_purpose).value
|
||||
};
|
||||
this.props.onDataChanged(newData);
|
||||
}
|
||||
render() {
|
||||
var displayNameError = null;
|
||||
var serverError = null;
|
||||
var displayNameClass = 'form-group';
|
||||
|
||||
if (this.state.displayNameError) {
|
||||
displayNameError = (
|
||||
<p className='input__help error'>
|
||||
<FormattedMessage
|
||||
id='channel_modal.displayNameError'
|
||||
defaultMessage='This field is required'
|
||||
/>
|
||||
{this.state.displayNameError}
|
||||
</p>
|
||||
);
|
||||
displayNameClass += ' has-error';
|
||||
}
|
||||
|
||||
if (this.props.serverError) {
|
||||
serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>;
|
||||
}
|
||||
|
||||
var channelTerm = '';
|
||||
var channelSwitchText = '';
|
||||
switch (this.props.channelType) {
|
||||
case 'P':
|
||||
channelTerm = (
|
||||
<FormattedMessage
|
||||
id='channel_modal.group'
|
||||
defaultMessage='Group'
|
||||
/>
|
||||
);
|
||||
channelSwitchText = (
|
||||
<div className='modal-intro'>
|
||||
<FormattedMessage
|
||||
id='channel_modal.privateGroup1'
|
||||
defaultMessage='Create a new private group with restricted membership. '
|
||||
/>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.props.onTypeSwitched}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_modal.publicChannel1'
|
||||
defaultMessage='Create a public channel'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
case 'O':
|
||||
channelTerm = (
|
||||
<FormattedMessage
|
||||
id='channel_modal.channel'
|
||||
defaultMessage='Channel'
|
||||
/>
|
||||
);
|
||||
channelSwitchText = (
|
||||
<div className='modal-intro'>
|
||||
<FormattedMessage
|
||||
id='channel_modal.publicChannel2'
|
||||
defaultMessage='Create a new public channel anyone can join. '
|
||||
/>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.props.onTypeSwitched}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_modal.privateGroup2'
|
||||
defaultMessage='Create a private group'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
const prettyTeamURL = Utils.getShortenedTeamURL();
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Modal
|
||||
show={this.props.show}
|
||||
bsSize='large'
|
||||
onHide={this.props.onModalDismissed}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='channel_modal.modalTitle'
|
||||
defaultMessage='New '
|
||||
/>
|
||||
{channelTerm}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<form
|
||||
role='form'
|
||||
className='form-horizontal'
|
||||
>
|
||||
<Modal.Body>
|
||||
<div>
|
||||
{channelSwitchText}
|
||||
</div>
|
||||
<div className={displayNameClass}>
|
||||
<label className='col-sm-3 form__label control-label'>
|
||||
<FormattedMessage
|
||||
id='channel_modal.name'
|
||||
defaultMessage='Name'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-9'>
|
||||
<input
|
||||
onChange={this.handleChange}
|
||||
type='text'
|
||||
ref='display_name'
|
||||
className='form-control'
|
||||
placeholder={this.props.intl.formatMessage(holders.nameEx)}
|
||||
maxLength='22'
|
||||
value={this.props.channelData.displayName}
|
||||
autoFocus={true}
|
||||
tabIndex='1'
|
||||
/>
|
||||
{displayNameError}
|
||||
<p className='input__help dark'>
|
||||
{'URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.props.onChangeURLPressed}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_modal.edit'
|
||||
defaultMessage='Edit'
|
||||
/>
|
||||
</a>
|
||||
{')'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group less'>
|
||||
<div className='col-sm-3'>
|
||||
<label className='form__label control-label'>
|
||||
<FormattedMessage
|
||||
id='channel_modal.purpose'
|
||||
defaultMessage='Purpose'
|
||||
/>
|
||||
</label>
|
||||
<label className='form__label light'>
|
||||
<FormattedMessage
|
||||
id='channel_modal.optional'
|
||||
defaultMessage='(optional)'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<div className='col-sm-9'>
|
||||
<textarea
|
||||
className='form-control no-resize'
|
||||
ref='channel_purpose'
|
||||
rows='4'
|
||||
placeholder={this.props.intl.formatMessage({id: 'channel_modal.purpose'})}
|
||||
maxLength='128'
|
||||
value={this.props.channelData.purpose}
|
||||
onChange={this.handleChange}
|
||||
tabIndex='2'
|
||||
/>
|
||||
<p className='input__help'>
|
||||
<FormattedMessage
|
||||
id='channel_modal.descriptionHelp'
|
||||
defaultMessage='Describe how this {term} should be used.'
|
||||
values={{
|
||||
term: (channelTerm)
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{serverError}
|
||||
</div>
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.props.onModalDismissed}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_modal.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={this.handleSubmit}
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
tabIndex='3'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_modal.createNew'
|
||||
defaultMessage='Create New '
|
||||
/>
|
||||
{channelTerm}
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NewChannelModal.defaultProps = {
|
||||
show: false,
|
||||
channelType: 'O',
|
||||
serverError: ''
|
||||
};
|
||||
NewChannelModal.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
channelType: React.PropTypes.string.isRequired,
|
||||
channelData: React.PropTypes.object.isRequired,
|
||||
serverError: React.PropTypes.string,
|
||||
onSubmitChannel: React.PropTypes.func.isRequired,
|
||||
onModalDismissed: React.PropTypes.func.isRequired,
|
||||
onTypeSwitched: React.PropTypes.func.isRequired,
|
||||
onChangeURLPressed: React.PropTypes.func.isRequired,
|
||||
onDataChanged: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(NewChannelModal);
|
||||
73
webapp/components/not_logged_in.jsx
Обычный файл
73
webapp/components/not_logged_in.jsx
Обычный файл
@@ -0,0 +1,73 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class NotLoggedIn extends React.Component {
|
||||
componentDidMount() {
|
||||
$('body').attr('class', 'sticky');
|
||||
$('#root').attr('class', 'container-fluid');
|
||||
}
|
||||
componentWillUnmount() {
|
||||
$('body').attr('class', '');
|
||||
$('#root').attr('class', '');
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className='inner-wrap'>
|
||||
<div className='row content'>
|
||||
{this.props.children}
|
||||
<div className='footer-push'></div>
|
||||
</div>
|
||||
<div className='row footer'>
|
||||
<div className='footer-pane col-xs-12'>
|
||||
<div className='col-xs-12'>
|
||||
<span className='pull-right footer-site-name'>{global.window.mm_config.SiteName}</span>
|
||||
</div>
|
||||
<div className='col-xs-12'>
|
||||
<span className='pull-right footer-link copyright'>{'© 2015 Mattermost, Inc.'}</span>
|
||||
<a
|
||||
id='help_link'
|
||||
className='pull-right footer-link'
|
||||
href={global.window.mm_config.HelpLink}
|
||||
>
|
||||
<FormattedMessage id='web.footer.help'/>
|
||||
</a>
|
||||
<a
|
||||
id='terms_link'
|
||||
className='pull-right footer-link'
|
||||
href={global.window.mm_config.TermsOfServiceLink}
|
||||
>
|
||||
<FormattedMessage id='web.footer.terms'/>
|
||||
</a>
|
||||
<a
|
||||
id='privacy_link'
|
||||
className='pull-right footer-link'
|
||||
href={global.window.mm_config.PrivacyPolicyLink}
|
||||
>
|
||||
<FormattedMessage id='web.footer.privacy'/>
|
||||
</a>
|
||||
<a
|
||||
id='about_link'
|
||||
className='pull-right footer-link'
|
||||
href={global.window.mm_config.AboutLink}
|
||||
>
|
||||
<FormattedMessage id='web.footer.about'/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
NotLoggedIn.defaultProps = {
|
||||
};
|
||||
|
||||
NotLoggedIn.propTypes = {
|
||||
children: React.PropTypes.object
|
||||
};
|
||||
54
webapp/components/notify_counts.jsx
Обычный файл
54
webapp/components/notify_counts.jsx
Обычный файл
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import * as utils from 'utils/utils.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
|
||||
function getCountsStateFromStores() {
|
||||
var count = 0;
|
||||
var channels = ChannelStore.getAll();
|
||||
var members = ChannelStore.getAllMembers();
|
||||
|
||||
channels.forEach(function setChannelInfo(channel) {
|
||||
var channelMember = members[channel.id];
|
||||
if (channel.type === 'D') {
|
||||
count += channel.total_msg_count - channelMember.msg_count;
|
||||
} else if (channelMember.mention_count > 0) {
|
||||
count += channelMember.mention_count;
|
||||
} else if (channelMember.notify_props.mark_unread !== 'mention' && channel.total_msg_count - channelMember.msg_count > 0) {
|
||||
count += 1;
|
||||
}
|
||||
});
|
||||
|
||||
return {count: count};
|
||||
}
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class NotifyCounts extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
|
||||
this.state = getCountsStateFromStores();
|
||||
}
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onListenerChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||
}
|
||||
onListenerChange() {
|
||||
var newState = getCountsStateFromStores();
|
||||
if (!utils.areObjectsEqual(newState, this.state)) {
|
||||
this.setState(newState);
|
||||
}
|
||||
}
|
||||
render() {
|
||||
if (this.state.count) {
|
||||
return <span className='badge badge-notify'>{this.state.count}</span>;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
132
webapp/components/password_reset_form.jsx
Обычный файл
132
webapp/components/password_reset_form.jsx
Обычный файл
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {browserHistory} from 'react-router';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class PasswordResetForm extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handlePasswordReset = this.handlePasswordReset.bind(this);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
handlePasswordReset(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const password = ReactDOM.findDOMNode(this.refs.password).value.trim();
|
||||
if (!password || password.length < Constants.MIN_PASSWORD_LENGTH) {
|
||||
this.setState({
|
||||
error: (
|
||||
<FormattedMessage
|
||||
id='password_form.error'
|
||||
defaultMessage='Please enter at least {chars} characters.'
|
||||
chars={Constants.MIN_PASSWORD_LENGTH}
|
||||
/>
|
||||
)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
error: null
|
||||
});
|
||||
|
||||
const data = {};
|
||||
data.new_password = password;
|
||||
data.hash = this.props.location.query.h;
|
||||
data.data = this.props.location.query.d;
|
||||
data.name = this.props.params.team;
|
||||
|
||||
Client.resetPassword(data,
|
||||
() => {
|
||||
this.setState({error: null});
|
||||
browserHistory.push('/' + this.props.params.team + '/login');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({error: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
render() {
|
||||
var error = null;
|
||||
if (this.state.error) {
|
||||
error = (
|
||||
<div className='form-group has-error'>
|
||||
<label className='control-label'>
|
||||
{this.state.error}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var formClass = 'form-group';
|
||||
if (error) {
|
||||
formClass += ' has-error';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='col-sm-12'>
|
||||
<div className='signup-team__container'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='password_form.title'
|
||||
defaultMessage='Password Reset'
|
||||
/>
|
||||
</h3>
|
||||
<form onSubmit={this.handlePasswordReset}>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='password_form.enter'
|
||||
defaultMessage='Enter a new password for your {siteName} account.'
|
||||
values={{
|
||||
siteName: global.window.mm_config.SiteName
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
<div className={formClass}>
|
||||
<input
|
||||
type='password'
|
||||
className='form-control'
|
||||
name='password'
|
||||
ref='password'
|
||||
placeholder={Utils.localizeMessage(
|
||||
'password_form.pwd',
|
||||
'Password'
|
||||
)}
|
||||
spellCheck='false'
|
||||
/>
|
||||
</div>
|
||||
{error}
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='password_form.change'
|
||||
defaultMessage='Change my password'
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PasswordResetForm.defaultProps = {
|
||||
};
|
||||
PasswordResetForm.propTypes = {
|
||||
params: React.PropTypes.object.isRequired,
|
||||
location: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default PasswordResetForm;
|
||||
153
webapp/components/password_reset_send_link.jsx
Обычный файл
153
webapp/components/password_reset_send_link.jsx
Обычный файл
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as client from 'utils/client.jsx';
|
||||
|
||||
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class PasswordResetSendLink extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleSendLink = this.handleSendLink.bind(this);
|
||||
|
||||
this.state = {
|
||||
error: '',
|
||||
updateText: ''
|
||||
};
|
||||
}
|
||||
handleSendLink(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var email = ReactDOM.findDOMNode(this.refs.email).value.trim().toLowerCase();
|
||||
if (!email || !Utils.isEmail(email)) {
|
||||
this.setState({
|
||||
error: (
|
||||
<FormattedMessage
|
||||
id={'password_send.error'}
|
||||
defaultMessage={'Please enter a valid email address.'}
|
||||
/>
|
||||
)
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// End of error checking clear error
|
||||
this.setState({
|
||||
error: ''
|
||||
});
|
||||
|
||||
var data = {};
|
||||
data.email = email;
|
||||
data.name = this.props.params.team;
|
||||
client.sendPasswordReset(data,
|
||||
() => {
|
||||
this.setState({
|
||||
error: null,
|
||||
updateText: (
|
||||
<div className='reset-form alert alert-success'>
|
||||
<FormattedHTMLMessage
|
||||
id='password_send.link'
|
||||
defaultMessage='<p>A password reset link has been sent to <b>{email}</b></p>'
|
||||
email={email}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id={'password_send.checkInbox'}
|
||||
defaultMessage={'Please check your inbox.'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
});
|
||||
$(ReactDOM.findDOMNode(this.refs.reset_form)).hide();
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
error: err.message,
|
||||
update_text: null
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
render() {
|
||||
var error = null;
|
||||
if (this.state.error) {
|
||||
error = <div className='form-group has-error'><label className='control-label'>{this.state.error}</label></div>;
|
||||
}
|
||||
|
||||
var formClass = 'form-group';
|
||||
if (error) {
|
||||
formClass += ' has-error';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='signup-header'>
|
||||
<a href='/'>
|
||||
<span className='fa fa-chevron-left'/>
|
||||
<FormattedMessage
|
||||
id='web.header.back'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className='col-sm-12'>
|
||||
<div className='signup-team__container'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='password_send.title'
|
||||
defaultMessage='Password Reset'
|
||||
/>
|
||||
</h3>
|
||||
{this.state.updateText}
|
||||
<form
|
||||
onSubmit={this.handleSendLink}
|
||||
ref='reset_form'
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id='password_send.description'
|
||||
defaultMessage='To reset your password, enter the email address you used to sign up'
|
||||
/>
|
||||
</p>
|
||||
<div className={formClass}>
|
||||
<input
|
||||
type='email'
|
||||
className='form-control'
|
||||
name='email'
|
||||
ref='email'
|
||||
placeholder={Utils.localizeMessage(
|
||||
'password_send.email',
|
||||
'Email'
|
||||
)}
|
||||
spellCheck='false'
|
||||
/>
|
||||
</div>
|
||||
{error}
|
||||
<button
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='password_send.reset'
|
||||
defaultMessage='Reset my password'
|
||||
/>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PasswordResetSendLink.defaultProps = {
|
||||
};
|
||||
PasswordResetSendLink.propTypes = {
|
||||
params: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default PasswordResetSendLink;
|
||||
175
webapp/components/popover_list_members.jsx
Обычный файл
175
webapp/components/popover_list_members.jsx
Обычный файл
@@ -0,0 +1,175 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import {Popover, Overlay} from 'react-bootstrap';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PopoverListMembers extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
|
||||
this.closePopover = this.closePopover.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({showPopover: false});
|
||||
}
|
||||
|
||||
handleShowDirectChannel(teammate, e) {
|
||||
e.preventDefault();
|
||||
|
||||
Utils.openDirectChannelToUser(
|
||||
teammate,
|
||||
(channel, channelAlreadyExisted) => {
|
||||
Utils.switchChannel(channel);
|
||||
if (channelAlreadyExisted) {
|
||||
this.closePopover();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.closePopover();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
closePopover() {
|
||||
this.setState({showPopover: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
const popoverHtml = [];
|
||||
const members = this.props.members;
|
||||
const teamMembers = UserStore.getProfilesUsernameMap();
|
||||
const currentUserId = UserStore.getCurrentId();
|
||||
const ch = ChannelStore.getCurrent();
|
||||
|
||||
if (members && teamMembers) {
|
||||
members.sort((a, b) => {
|
||||
const aName = Utils.displayUsername(a.id);
|
||||
const bName = Utils.displayUsername(b.id);
|
||||
|
||||
return aName.localeCompare(bName);
|
||||
});
|
||||
|
||||
members.forEach((m, i) => {
|
||||
let button = '';
|
||||
if (currentUserId !== m.id && ch.type !== 'D') {
|
||||
button = (
|
||||
<a
|
||||
href='#'
|
||||
className='btn-message'
|
||||
onClick={(e) => this.handleShowDirectChannel(m, e)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='members_popover.msg'
|
||||
defaultMessage='Message'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
let name = '';
|
||||
if (teamMembers[m.username]) {
|
||||
name = Utils.displayUsername(teamMembers[m.username].id);
|
||||
}
|
||||
|
||||
if (name) {
|
||||
popoverHtml.push(
|
||||
<div
|
||||
className='more-modal__row'
|
||||
key={'popover-member-' + i}
|
||||
>
|
||||
|
||||
<img
|
||||
className='more-modal__image'
|
||||
width='26px'
|
||||
height='26px'
|
||||
src={`/api/v1/users/${m.id}/image?time=${m.update_at}`}
|
||||
/>
|
||||
<div className='more-modal__details'>
|
||||
<div
|
||||
className='more-modal__name'
|
||||
>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='more-modal__actions'
|
||||
>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let count = this.props.memberCount;
|
||||
let countText = '-';
|
||||
|
||||
// fall back to checking the length of the member list if the count isn't set
|
||||
if (!count && members) {
|
||||
count = members.length;
|
||||
}
|
||||
|
||||
if (count > Constants.MAX_CHANNEL_POPOVER_COUNT) {
|
||||
countText = Constants.MAX_CHANNEL_POPOVER_COUNT + '+';
|
||||
} else if (count > 0) {
|
||||
countText = count.toString();
|
||||
}
|
||||
|
||||
const title = (
|
||||
<FormattedMessage
|
||||
id='members_popover.title'
|
||||
defaultMessage='Members'
|
||||
/>
|
||||
);
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
id='member_popover'
|
||||
ref='member_popover_target'
|
||||
onClick={(e) => this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover})}
|
||||
>
|
||||
<div>
|
||||
{countText}
|
||||
<span
|
||||
className='fa fa-user'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Overlay
|
||||
rootClose={true}
|
||||
onHide={this.closePopover}
|
||||
show={this.state.showPopover}
|
||||
target={() => this.state.popoverTarget}
|
||||
placement='bottom'
|
||||
>
|
||||
<Popover
|
||||
ref='memebersPopover'
|
||||
title={title}
|
||||
id='member-list-popover'
|
||||
>
|
||||
<div className='more-modal__list'>{popoverHtml}</div>
|
||||
</Popover>
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PopoverListMembers.propTypes = {
|
||||
members: React.PropTypes.array.isRequired,
|
||||
memberCount: React.PropTypes.number,
|
||||
channelId: React.PropTypes.string.isRequired
|
||||
};
|
||||
254
webapp/components/post.jsx
Обычный файл
254
webapp/components/post.jsx
Обычный файл
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostHeader from './post_header.jsx';
|
||||
import PostBody from './post_body.jsx';
|
||||
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
|
||||
import * as Client from 'utils/client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class Post extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleCommentClick = this.handleCommentClick.bind(this);
|
||||
this.forceUpdateInfo = this.forceUpdateInfo.bind(this);
|
||||
this.retryPost = this.retryPost.bind(this);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
handleCommentClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_POST_SELECTED,
|
||||
postId: Utils.getRootId(this.props.post)
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_SEARCH,
|
||||
results: null
|
||||
});
|
||||
}
|
||||
forceUpdateInfo() {
|
||||
this.refs.info.forceUpdate();
|
||||
this.refs.header.forceUpdate();
|
||||
}
|
||||
retryPost(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var post = this.props.post;
|
||||
Client.createPost(post, post.channel_id,
|
||||
(data) => {
|
||||
AsyncClient.getPosts();
|
||||
|
||||
var channel = ChannelStore.get(post.channel_id);
|
||||
var member = ChannelStore.getMember(post.channel_id);
|
||||
member.msg_count = channel.total_msg_count;
|
||||
member.last_viewed_at = Utils.getTimestamp();
|
||||
ChannelStore.setChannelMember(member);
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_POST,
|
||||
post: data
|
||||
});
|
||||
},
|
||||
() => {
|
||||
post.state = Constants.POST_FAILED;
|
||||
PostStore.updatePendingPost(post);
|
||||
this.forceUpdate();
|
||||
}
|
||||
);
|
||||
|
||||
post.state = Constants.POST_LOADING;
|
||||
PostStore.updatePendingPost(post);
|
||||
this.forceUpdate();
|
||||
}
|
||||
shouldComponentUpdate(nextProps) {
|
||||
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.sameRoot !== this.props.sameRoot) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.sameUser !== this.props.sameUser) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.displayNameType !== this.props.displayNameType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.getCommentCount(nextProps) !== this.getCommentCount(this.props)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (nextProps.shouldHighlight !== this.props.shouldHighlight) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(nextProps.user, this.props.user)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
getCommentCount(props) {
|
||||
const post = props.post;
|
||||
const parentPost = props.parentPost;
|
||||
const posts = props.posts;
|
||||
|
||||
let commentCount = 0;
|
||||
let commentRootId;
|
||||
if (parentPost) {
|
||||
commentRootId = post.root_id;
|
||||
} else {
|
||||
commentRootId = post.id;
|
||||
}
|
||||
for (const postId in posts) {
|
||||
if (posts[postId].root_id === commentRootId) {
|
||||
commentCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return commentCount;
|
||||
}
|
||||
render() {
|
||||
const post = this.props.post;
|
||||
const parentPost = this.props.parentPost;
|
||||
const posts = this.props.posts;
|
||||
|
||||
if (!post.props) {
|
||||
post.props = {};
|
||||
}
|
||||
|
||||
let type = 'Post';
|
||||
if (post.root_id && post.root_id.length > 0) {
|
||||
type = 'Comment';
|
||||
}
|
||||
|
||||
const commentCount = this.getCommentCount(this.props);
|
||||
|
||||
let rootUser;
|
||||
if (this.props.sameRoot) {
|
||||
rootUser = 'same--root';
|
||||
} else {
|
||||
rootUser = 'other--root';
|
||||
}
|
||||
|
||||
let postType = '';
|
||||
if (type !== 'Post') {
|
||||
postType = 'post--comment';
|
||||
} else if (commentCount > 0) {
|
||||
postType = 'post--root';
|
||||
}
|
||||
|
||||
let currentUserCss = '';
|
||||
if (this.props.currentUser.id === post.user_id && !post.props.from_webhook && !Utils.isSystemMessage(post)) {
|
||||
currentUserCss = 'current--user';
|
||||
}
|
||||
|
||||
let timestamp = 0;
|
||||
if (!this.props.user || this.props.user.update_at == null) {
|
||||
timestamp = this.props.currentUser.update_at;
|
||||
} else {
|
||||
timestamp = this.props.user.update_at;
|
||||
}
|
||||
|
||||
let sameUserClass = '';
|
||||
if (this.props.sameUser) {
|
||||
sameUserClass = 'same--user';
|
||||
}
|
||||
|
||||
let shouldHighlightClass = '';
|
||||
if (this.props.shouldHighlight) {
|
||||
shouldHighlightClass = 'post--highlight';
|
||||
}
|
||||
|
||||
let systemMessageClass = '';
|
||||
if (Utils.isSystemMessage(post)) {
|
||||
systemMessageClass = 'post--system';
|
||||
}
|
||||
|
||||
let profilePic = null;
|
||||
if (!this.props.hideProfilePic) {
|
||||
let src = '/api/v1/users/' + post.user_id + '/image?time=' + timestamp;
|
||||
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)) {
|
||||
src = Constants.SYSTEM_MESSAGE_PROFILE_IMAGE;
|
||||
}
|
||||
|
||||
profilePic = (
|
||||
<img
|
||||
src={src}
|
||||
height='36'
|
||||
width='36'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
id={'post_' + post.id}
|
||||
className={'post ' + sameUserClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass}
|
||||
>
|
||||
<div className='post__content'>
|
||||
<div className='post__img'>{profilePic}</div>
|
||||
<div>
|
||||
<PostHeader
|
||||
ref='header'
|
||||
post={post}
|
||||
sameRoot={this.props.sameRoot}
|
||||
commentCount={commentCount}
|
||||
handleCommentClick={this.handleCommentClick}
|
||||
isLastComment={this.props.isLastComment}
|
||||
sameUser={this.props.sameUser}
|
||||
user={this.props.user}
|
||||
currentUser={this.props.currentUser}
|
||||
/>
|
||||
<PostBody
|
||||
post={post}
|
||||
sameRoot={this.props.sameRoot}
|
||||
parentPost={parentPost}
|
||||
posts={posts}
|
||||
handleCommentClick={this.handleCommentClick}
|
||||
retryPost={this.retryPost}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Post.propTypes = {
|
||||
post: React.PropTypes.object.isRequired,
|
||||
posts: React.PropTypes.object,
|
||||
parentPost: React.PropTypes.object,
|
||||
user: React.PropTypes.object,
|
||||
sameUser: React.PropTypes.bool,
|
||||
sameRoot: React.PropTypes.bool,
|
||||
hideProfilePic: React.PropTypes.bool,
|
||||
isLastComment: React.PropTypes.bool,
|
||||
shouldHighlight: React.PropTypes.bool,
|
||||
displayNameType: React.PropTypes.string,
|
||||
hasProfiles: React.PropTypes.bool,
|
||||
currentUser: React.PropTypes.object.isRequired
|
||||
};
|
||||
315
webapp/components/post_attachment.jsx
Обычный файл
315
webapp/components/post_attachment.jsx
Обычный файл
@@ -0,0 +1,315 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
collapse: {
|
||||
id: 'post_attachment.collapse',
|
||||
defaultMessage: '▲ collapse text'
|
||||
},
|
||||
more: {
|
||||
id: 'post_attachment.more',
|
||||
defaultMessage: '▼ read more'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class PostAttachment extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getFieldsTable = this.getFieldsTable.bind(this);
|
||||
this.getInitState = this.getInitState.bind(this);
|
||||
this.shouldCollapse = this.shouldCollapse.bind(this);
|
||||
this.toggleCollapseState = this.toggleCollapseState.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
$(this.refs.attachment).on('click', '.attachment-link-more', this.toggleCollapseState);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
$(this.refs.attachment).off('click', '.attachment-link-more', this.toggleCollapseState);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState(this.getInitState());
|
||||
}
|
||||
|
||||
getInitState() {
|
||||
const shouldCollapse = this.shouldCollapse();
|
||||
const text = TextFormatting.formatText(this.props.attachment.text || '');
|
||||
const uncollapsedText = text + (shouldCollapse ? `<a class="attachment-link-more" href="#">${this.props.intl.formatMessage(holders.collapse)}</a>` : '');
|
||||
const collapsedText = shouldCollapse ? this.getCollapsedText() : text;
|
||||
|
||||
return {
|
||||
shouldCollapse,
|
||||
collapsedText,
|
||||
uncollapsedText,
|
||||
text: shouldCollapse ? collapsedText : uncollapsedText,
|
||||
collapsed: shouldCollapse
|
||||
};
|
||||
}
|
||||
|
||||
toggleCollapseState(e) {
|
||||
e.preventDefault();
|
||||
|
||||
let state = this.state;
|
||||
state.text = state.collapsed ? state.uncollapsedText : state.collapsedText;
|
||||
state.collapsed = !state.collapsed;
|
||||
this.setState(state);
|
||||
}
|
||||
|
||||
shouldCollapse() {
|
||||
const text = this.props.attachment.text || '';
|
||||
return (text.match(/\n/g) || []).length >= 5 || text.length > 700;
|
||||
}
|
||||
|
||||
getCollapsedText() {
|
||||
let text = this.props.attachment.text || '';
|
||||
if ((text.match(/\n/g) || []).length >= 5) {
|
||||
text = text.split('\n').splice(0, 5).join('\n');
|
||||
} else if (text.length > 700) {
|
||||
text = text.substr(0, 700);
|
||||
}
|
||||
|
||||
return TextFormatting.formatText(text) + `<a class="attachment-link-more" href="#">${this.props.intl.formatMessage(holders.more)}</a>`;
|
||||
}
|
||||
|
||||
getFieldsTable() {
|
||||
const fields = this.props.attachment.fields;
|
||||
if (!fields || !fields.length) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const compactTable = fields.filter((field) => field.short).length > 0;
|
||||
let tHead;
|
||||
let tBody;
|
||||
|
||||
if (compactTable) {
|
||||
let headerCols = [];
|
||||
let bodyCols = [];
|
||||
|
||||
fields.forEach((field, i) => {
|
||||
headerCols.push(
|
||||
<th
|
||||
className='attachment___field-caption'
|
||||
key={'attachment__field-caption-' + i}
|
||||
>
|
||||
{field.title}
|
||||
</th>
|
||||
);
|
||||
bodyCols.push(
|
||||
<td
|
||||
className='attachment___field'
|
||||
key={'attachment__field-' + i}
|
||||
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(field.value || '')}}
|
||||
>
|
||||
</td>
|
||||
);
|
||||
});
|
||||
|
||||
tHead = (
|
||||
<tr>
|
||||
{headerCols}
|
||||
</tr>
|
||||
);
|
||||
tBody = (
|
||||
<tr>
|
||||
{bodyCols}
|
||||
</tr>
|
||||
);
|
||||
} else {
|
||||
tBody = [];
|
||||
|
||||
fields.forEach((field, i) => {
|
||||
tBody.push(
|
||||
<tr key={'attachment__field-' + i}>
|
||||
<td
|
||||
className='attachment___field-caption'
|
||||
>
|
||||
{field.title}
|
||||
</td>
|
||||
<td
|
||||
className='attachment___field'
|
||||
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(field.value || '')}}
|
||||
>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<table
|
||||
className='attachment___fields'
|
||||
>
|
||||
<thead>
|
||||
{tHead}
|
||||
</thead>
|
||||
<tbody>
|
||||
{tBody}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const data = this.props.attachment;
|
||||
|
||||
let preText;
|
||||
if (data.pretext) {
|
||||
preText = (
|
||||
<div
|
||||
className='attachment__thumb-pretext'
|
||||
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(data.pretext)}}
|
||||
>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let author = [];
|
||||
if (data.author_name || data.author_icon) {
|
||||
if (data.author_icon) {
|
||||
author.push(
|
||||
<img
|
||||
className='attachment__author-icon'
|
||||
src={data.author_icon}
|
||||
key={'attachment__author-icon'}
|
||||
height='14'
|
||||
width='14'
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (data.author_name) {
|
||||
author.push(
|
||||
<span
|
||||
className='attachment__author-name'
|
||||
key={'attachment__author-name'}
|
||||
>
|
||||
{data.author_name}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
if (data.author_link) {
|
||||
author = (
|
||||
<a
|
||||
href={data.author_link}
|
||||
target='_blank'
|
||||
>
|
||||
{author}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
let title;
|
||||
if (data.title) {
|
||||
if (data.title_link) {
|
||||
title = (
|
||||
<h1
|
||||
className='attachment__title'
|
||||
>
|
||||
<a
|
||||
className='attachment__title-link'
|
||||
href={data.title_link}
|
||||
target='_blank'
|
||||
>
|
||||
{data.title}
|
||||
</a>
|
||||
</h1>
|
||||
);
|
||||
} else {
|
||||
title = (
|
||||
<h1
|
||||
className='attachment__title'
|
||||
>
|
||||
{data.title}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let text;
|
||||
if (data.text) {
|
||||
text = (
|
||||
<div
|
||||
className='attachment__text'
|
||||
dangerouslySetInnerHTML={{__html: this.state.text}}
|
||||
>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let image;
|
||||
if (data.image_url) {
|
||||
image = (
|
||||
<img
|
||||
className='attachment__image'
|
||||
src={data.image_url}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let thumb;
|
||||
if (data.thumb_url) {
|
||||
thumb = (
|
||||
<div
|
||||
className='attachment__thumb-container'
|
||||
>
|
||||
<img
|
||||
src={data.thumb_url}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const fields = this.getFieldsTable();
|
||||
|
||||
let useBorderStyle;
|
||||
if (data.color && data.color[0] === '#') {
|
||||
useBorderStyle = {borderLeftColor: data.color};
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='attachment'
|
||||
ref='attachment'
|
||||
>
|
||||
{preText}
|
||||
<div className='attachment__content'>
|
||||
<div
|
||||
className={useBorderStyle ? 'clearfix attachment__container' : 'clearfix attachment__container attachment__container--' + data.color}
|
||||
style={useBorderStyle}
|
||||
>
|
||||
{author}
|
||||
{title}
|
||||
<div>
|
||||
<div
|
||||
className={thumb ? 'attachment__body' : 'attachment__body attachment__body--no_thumb'}
|
||||
>
|
||||
{text}
|
||||
{image}
|
||||
{fields}
|
||||
</div>
|
||||
{thumb}
|
||||
<div style={{clear: 'both'}}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostAttachment.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
attachment: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(PostAttachment);
|
||||
30
webapp/components/post_attachment_list.jsx
Обычный файл
30
webapp/components/post_attachment_list.jsx
Обычный файл
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostAttachment from './post_attachment.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostAttachmentList extends React.Component {
|
||||
render() {
|
||||
let content = [];
|
||||
this.props.attachments.forEach((attachment, i) => {
|
||||
content.push(
|
||||
<PostAttachment
|
||||
attachment={attachment}
|
||||
key={'att_' + i}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className='attachment_list'>
|
||||
{content}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostAttachmentList.propTypes = {
|
||||
attachments: React.PropTypes.array.isRequired
|
||||
};
|
||||
107
webapp/components/post_attachment_oembed.jsx
Обычный файл
107
webapp/components/post_attachment_oembed.jsx
Обычный файл
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import React from 'react';
|
||||
|
||||
export default class PostAttachmentOEmbed extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.fetchData = this.fetchData.bind(this);
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({data: {}});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.link !== this.props.link) {
|
||||
this.isLoading = false;
|
||||
this.fetchData(nextProps.link);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchData(this.props.link);
|
||||
}
|
||||
|
||||
fetchData(link) {
|
||||
if (!this.isLoading) {
|
||||
this.isLoading = true;
|
||||
let url = 'https://noembed.com/embed?nowrap=on';
|
||||
url += '&url=' + encodeURIComponent(link);
|
||||
url += '&maxheight=' + this.props.provider.height;
|
||||
return $.ajax({
|
||||
url,
|
||||
dataType: 'jsonp',
|
||||
success: (result) => {
|
||||
this.isLoading = false;
|
||||
if (result.error) {
|
||||
this.setState({data: {}});
|
||||
} else {
|
||||
this.setState({data: result});
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.setState({data: {}});
|
||||
}
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
let data = {};
|
||||
let content;
|
||||
if ($.isEmptyObject(this.state.data)) {
|
||||
content = <div style={{height: this.props.provider.height}}/>;
|
||||
} else {
|
||||
data = this.state.data;
|
||||
content = (
|
||||
<div
|
||||
style={{height: this.props.provider.height}}
|
||||
dangerouslySetInnerHTML={{__html: data.html}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className='attachment attachment--oembed'
|
||||
ref='attachment'
|
||||
>
|
||||
<div className='attachment__content'>
|
||||
<div
|
||||
className={'clearfix attachment__container'}
|
||||
>
|
||||
<h1
|
||||
className='attachment__title'
|
||||
>
|
||||
<a
|
||||
className='attachment__title-link'
|
||||
href={data.url}
|
||||
target='_blank'
|
||||
>
|
||||
{data.title}
|
||||
</a>
|
||||
</h1>
|
||||
<div >
|
||||
<div
|
||||
className={'attachment__body attachment__body--no_thumb'}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostAttachmentOEmbed.propTypes = {
|
||||
link: React.PropTypes.string.isRequired,
|
||||
provider: React.PropTypes.object.isRequired
|
||||
};
|
||||
224
webapp/components/post_body.jsx
Обычный файл
224
webapp/components/post_body.jsx
Обычный файл
@@ -0,0 +1,224 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import ReactDOM from 'react-dom';
|
||||
import FileAttachmentList from './file_attachment_list.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as Emoji from 'utils/emoticons.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import * as TextFormatting from 'utils/text_formatting.jsx';
|
||||
import twemoji from 'twemoji';
|
||||
import PostBodyAdditionalContent from './post_body_additional_content.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
import loadingGif from 'images/load.gif';
|
||||
|
||||
const holders = defineMessages({
|
||||
plusOne: {
|
||||
id: 'post_body.plusOne',
|
||||
defaultMessage: ' plus 1 other file'
|
||||
},
|
||||
plusMore: {
|
||||
id: 'post_body.plusMore',
|
||||
defaultMessage: ' plus {count} other files'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class PostBody extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.parseEmojis = this.parseEmojis.bind(this);
|
||||
}
|
||||
|
||||
getAllChildNodes(nodeIn) {
|
||||
var textNodes = [];
|
||||
|
||||
function getTextNodes(node) {
|
||||
textNodes.push(node);
|
||||
|
||||
for (var i = 0, len = node.childNodes.length; i < len; ++i) {
|
||||
getTextNodes(node.childNodes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
getTextNodes(nodeIn);
|
||||
return textNodes;
|
||||
}
|
||||
|
||||
parseEmojis() {
|
||||
twemoji.parse(ReactDOM.findDOMNode(this), {
|
||||
className: 'emoji twemoji',
|
||||
base: '',
|
||||
folder: Emoji.getImagePathForEmoticon()
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.parseEmojis();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
const post = this.props.post;
|
||||
const filenames = this.props.post.filenames;
|
||||
const parentPost = this.props.parentPost;
|
||||
|
||||
let comment = '';
|
||||
let postClass = '';
|
||||
|
||||
if (parentPost) {
|
||||
const profile = UserStore.getProfile(parentPost.user_id);
|
||||
|
||||
let apostrophe = '';
|
||||
let name = '...';
|
||||
if (profile != null) {
|
||||
let username = profile.username;
|
||||
if (parentPost.props &&
|
||||
parentPost.props.from_webhook &&
|
||||
parentPost.props.override_username &&
|
||||
global.window.mm_config.EnablePostUsernameOverride === 'true') {
|
||||
username = parentPost.props.override_username;
|
||||
}
|
||||
|
||||
if (username.slice(-1) === 's') {
|
||||
apostrophe = '\'';
|
||||
} else {
|
||||
apostrophe = '\'s';
|
||||
}
|
||||
name = (
|
||||
<a
|
||||
className='theme'
|
||||
onClick={Utils.searchForTerm.bind(null, username)}
|
||||
>
|
||||
{username}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
let message = '';
|
||||
if (parentPost.message) {
|
||||
message = Utils.replaceHtmlEntities(parentPost.message);
|
||||
} else if (parentPost.filenames.length) {
|
||||
message = parentPost.filenames[0].split('/').pop();
|
||||
|
||||
if (parentPost.filenames.length === 2) {
|
||||
message += formatMessage(holders.plusOne);
|
||||
} else if (parentPost.filenames.length > 2) {
|
||||
message += formatMessage(holders.plusMore, {count: (parentPost.filenames.length - 1)});
|
||||
}
|
||||
}
|
||||
|
||||
comment = (
|
||||
<div className='post__link'>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='post_body.commentedOn'
|
||||
defaultMessage='Commented on {name}{apostrophe} message: '
|
||||
values={{
|
||||
name: (name),
|
||||
apostrophe: apostrophe
|
||||
}}
|
||||
/>
|
||||
<a
|
||||
className='theme'
|
||||
onClick={this.props.handleCommentClick}
|
||||
>
|
||||
{message}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let loading;
|
||||
if (post.state === Constants.POST_FAILED) {
|
||||
postClass += ' post--fail';
|
||||
loading = (
|
||||
<a
|
||||
className='theme post-retry pull-right'
|
||||
href='#'
|
||||
onClick={this.props.retryPost}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='post_body.retry'
|
||||
defaultMessage='Retry'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
} else if (post.state === Constants.POST_LOADING) {
|
||||
postClass += ' post-waiting';
|
||||
loading = (
|
||||
<img
|
||||
className='post-loading-gif pull-right'
|
||||
src={loadingGif}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let fileAttachmentHolder = '';
|
||||
if (filenames && filenames.length > 0) {
|
||||
fileAttachmentHolder = (
|
||||
<FileAttachmentList
|
||||
filenames={filenames}
|
||||
channelId={post.channel_id}
|
||||
userId={post.user_id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let message;
|
||||
let additionalContent = null;
|
||||
if (this.props.post.state === Constants.POST_DELETED) {
|
||||
message = (
|
||||
<FormattedMessage
|
||||
id='post_body.deleted'
|
||||
defaultMessage='(message deleted)'
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
message = (
|
||||
<span
|
||||
onClick={TextFormatting.handleClick}
|
||||
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message)}}
|
||||
/>
|
||||
);
|
||||
|
||||
additionalContent = (
|
||||
<PostBodyAdditionalContent post={this.props.post}/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{comment}
|
||||
<div className='post__body'>
|
||||
<div
|
||||
key={`${post.id}_message`}
|
||||
id={`${post.id}_message`}
|
||||
className={postClass}
|
||||
>
|
||||
{loading}
|
||||
{message}
|
||||
</div>
|
||||
{fileAttachmentHolder}
|
||||
{additionalContent}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostBody.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
post: React.PropTypes.object.isRequired,
|
||||
parentPost: React.PropTypes.object,
|
||||
retryPost: React.PropTypes.func.isRequired,
|
||||
handleCommentClick: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(PostBody);
|
||||
149
webapp/components/post_body_additional_content.jsx
Обычный файл
149
webapp/components/post_body_additional_content.jsx
Обычный файл
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostAttachmentList from './post_attachment_list.jsx';
|
||||
import PostAttachmentOEmbed from './post_attachment_oembed.jsx';
|
||||
import PostImage from './post_image.jsx';
|
||||
import YoutubeVideo from './youtube_video.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import OEmbedProviders from './providers.json';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostBodyAdditionalContent extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getSlackAttachment = this.getSlackAttachment.bind(this);
|
||||
this.getOEmbedProvider = this.getOEmbedProvider.bind(this);
|
||||
this.generateEmbed = this.generateEmbed.bind(this);
|
||||
this.toggleEmbedVisibility = this.toggleEmbedVisibility.bind(this);
|
||||
|
||||
this.state = {
|
||||
embedVisible: true
|
||||
};
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(nextProps.post, this.props.post)) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.embedVisible !== this.state.embedVisible) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
toggleEmbedVisibility() {
|
||||
this.setState({embedVisible: !this.state.embedVisible});
|
||||
}
|
||||
|
||||
getSlackAttachment() {
|
||||
let attachments = [];
|
||||
if (this.props.post.props && this.props.post.props.attachments) {
|
||||
attachments = this.props.post.props.attachments;
|
||||
}
|
||||
|
||||
return (
|
||||
<PostAttachmentList
|
||||
attachments={attachments}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getOEmbedProvider(link) {
|
||||
for (let i = 0; i < OEmbedProviders.length; i++) {
|
||||
for (let j = 0; j < OEmbedProviders[i].patterns.length; j++) {
|
||||
if (link.match(OEmbedProviders[i].patterns[j])) {
|
||||
return OEmbedProviders[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
generateEmbed() {
|
||||
if (this.props.post.type === 'slack_attachment') {
|
||||
return this.getSlackAttachment();
|
||||
}
|
||||
|
||||
const link = Utils.extractLinks(this.props.post.message)[0];
|
||||
if (!link) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) {
|
||||
const provider = this.getOEmbedProvider(link);
|
||||
|
||||
if (provider) {
|
||||
return (
|
||||
<PostAttachmentOEmbed
|
||||
provider={provider}
|
||||
link={link}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (YoutubeVideo.isYoutubeLink(link)) {
|
||||
return (
|
||||
<YoutubeVideo
|
||||
channelId={this.props.post.channel_id}
|
||||
link={link}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < Constants.IMAGE_TYPES.length; i++) {
|
||||
const imageType = Constants.IMAGE_TYPES[i];
|
||||
const suffix = link.substring(link.length - (imageType.length + 1));
|
||||
if (suffix === '.' + imageType || suffix === '=' + imageType) {
|
||||
return (
|
||||
<PostImage
|
||||
channelId={this.props.post.channel_id}
|
||||
link={link}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
const generateEmbed = this.generateEmbed();
|
||||
|
||||
if (generateEmbed) {
|
||||
let toggle;
|
||||
if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_TOGGLE)) {
|
||||
toggle = (
|
||||
<a className='post__embed-visibility'
|
||||
data-expanded={this.state.embedVisible}
|
||||
aria-label='Toggle Embed Visibility'
|
||||
onClick={this.toggleEmbedVisibility}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{toggle}
|
||||
<div className='post__embed-container'
|
||||
hidden={!this.state.embedVisible}
|
||||
>
|
||||
{generateEmbed}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
PostBodyAdditionalContent.propTypes = {
|
||||
post: React.PropTypes.object.isRequired
|
||||
};
|
||||
91
webapp/components/post_deleted_modal.jsx
Обычный файл
91
webapp/components/post_deleted_modal.jsx
Обычный файл
@@ -0,0 +1,91 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostDeletedModal extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleHide = this.handleHide.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.show !== this.props.show;
|
||||
}
|
||||
|
||||
handleHide(e) {
|
||||
e.preventDefault();
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_SEARCH,
|
||||
results: null
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_SEARCH_TERM,
|
||||
term: null,
|
||||
do_search: false,
|
||||
is_mention_search: false
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_POST_SELECTED,
|
||||
postId: null
|
||||
});
|
||||
|
||||
this.props.onHide();
|
||||
}
|
||||
|
||||
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
|
||||
};
|
||||
135
webapp/components/post_focus_view.jsx
Обычный файл
135
webapp/components/post_focus_view.jsx
Обычный файл
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostsView from './posts_view.jsx';
|
||||
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostFocusView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.onChannelChange = this.onChannelChange.bind(this);
|
||||
this.onPostsChange = this.onPostsChange.bind(this);
|
||||
this.onUserChange = this.onUserChange.bind(this);
|
||||
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
|
||||
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
|
||||
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
|
||||
|
||||
const focusedPostId = PostStore.getFocusedPostId();
|
||||
|
||||
this.state = {
|
||||
scrollType: PostsView.SCROLL_TYPE_POST,
|
||||
scrollPostId: focusedPostId,
|
||||
postList: PostStore.getVisiblePosts(focusedPostId),
|
||||
atTop: PostStore.getVisibilityAtTop(focusedPostId),
|
||||
atBottom: PostStore.getVisibilityAtBottom(focusedPostId),
|
||||
currentUser: UserStore.getCurrentUser()
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onChannelChange);
|
||||
PostStore.addChangeListener(this.onPostsChange);
|
||||
UserStore.addChangeListener(this.onUserChange);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
||||
PostStore.removeChangeListener(this.onPostsChange);
|
||||
UserStore.removeChangeListener(this.onUserChange);
|
||||
}
|
||||
|
||||
onChannelChange() {
|
||||
this.setState({
|
||||
scrollType: PostsView.SCROLL_TYPE_POST
|
||||
});
|
||||
}
|
||||
|
||||
onUserChange() {
|
||||
this.setState({currentUser: UserStore.getCurrentUser()});
|
||||
}
|
||||
|
||||
onPostsChange() {
|
||||
const focusedPostId = PostStore.getFocusedPostId();
|
||||
if (focusedPostId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
scrollPostId: focusedPostId,
|
||||
postList: PostStore.getVisiblePosts(focusedPostId),
|
||||
atTop: PostStore.getVisibilityAtTop(focusedPostId),
|
||||
atBottom: PostStore.getVisibilityAtBottom(focusedPostId)
|
||||
});
|
||||
}
|
||||
|
||||
handlePostsViewScroll() {
|
||||
this.setState({scrollType: PostsView.SCROLL_TYPE_FREE});
|
||||
}
|
||||
|
||||
loadMorePostsTop() {
|
||||
GlobalActions.emitLoadMorePostsFocusedTopEvent();
|
||||
}
|
||||
|
||||
loadMorePostsBottom() {
|
||||
GlobalActions.emitLoadMorePostsFocusedBottomEvent();
|
||||
}
|
||||
|
||||
getIntroMessage() {
|
||||
return (
|
||||
<div className='channel-intro'>
|
||||
<h4 className='channel-intro__title'>
|
||||
<FormattedMessage
|
||||
id='post_focus_view.beginning'
|
||||
defaultMessage='Beginning of Channel Archives'
|
||||
/>
|
||||
</h4>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const postsToHighlight = {};
|
||||
postsToHighlight[this.state.scrollPostId] = true;
|
||||
|
||||
if (!this.state.currentUser || !this.state.postList) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='post-list'>
|
||||
<PostsView
|
||||
key={'postfocusview'}
|
||||
isActive={true}
|
||||
postList={this.state.postList}
|
||||
scrollType={this.state.scrollType}
|
||||
scrollPostId={this.state.scrollPostId}
|
||||
postViewScrolled={this.handlePostsViewScroll}
|
||||
loadMorePostsTopClicked={this.loadMorePostsTop}
|
||||
loadMorePostsBottomClicked={this.loadMorePostsBottom}
|
||||
showMoreMessagesTop={!this.state.atTop}
|
||||
showMoreMessagesBottom={!this.state.atBottom}
|
||||
introText={this.getIntroMessage()}
|
||||
messageSeparatorTime={0}
|
||||
postsToHighlight={postsToHighlight}
|
||||
profiles={this.props.profiles}
|
||||
currentUser={this.state.currentUser}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
PostFocusView.defaultProps = {
|
||||
};
|
||||
|
||||
PostFocusView.propTypes = {
|
||||
profiles: React.PropTypes.object
|
||||
};
|
||||
80
webapp/components/post_header.jsx
Обычный файл
80
webapp/components/post_header.jsx
Обычный файл
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import UserProfile from './user_profile.jsx';
|
||||
import PostInfo from './post_info.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostHeader extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
render() {
|
||||
const post = this.props.post;
|
||||
|
||||
let userProfile = <UserProfile user={this.props.user}/>;
|
||||
let botIndicator;
|
||||
|
||||
if (post.props && post.props.from_webhook) {
|
||||
if (post.props.override_username && global.window.mm_config.EnablePostUsernameOverride === 'true') {
|
||||
userProfile = (
|
||||
<UserProfile
|
||||
user={this.props.user}
|
||||
overwriteName={post.props.override_username}
|
||||
disablePopover={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
botIndicator = <li className='col col__name bot-indicator'>{'BOT'}</li>;
|
||||
} else if (Utils.isSystemMessage(post)) {
|
||||
userProfile = (
|
||||
<UserProfile
|
||||
user={{}}
|
||||
overwriteName={Constants.SYSTEM_MESSAGE_PROFILE_NAME}
|
||||
overwriteImage={Constants.SYSTEM_MESSAGE_PROFILE_IMAGE}
|
||||
disablePopover={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ul className='post__header'>
|
||||
<li className='col col__name'>{userProfile}</li>
|
||||
{botIndicator}
|
||||
<li className='col'>
|
||||
<PostInfo
|
||||
post={post}
|
||||
commentCount={this.props.commentCount}
|
||||
handleCommentClick={this.props.handleCommentClick}
|
||||
allowReply='true'
|
||||
isLastComment={this.props.isLastComment}
|
||||
sameUser={this.props.sameUser}
|
||||
currentUser={this.props.currentUser}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostHeader.defaultProps = {
|
||||
post: null,
|
||||
commentCount: 0,
|
||||
isLastComment: false,
|
||||
sameUser: false
|
||||
};
|
||||
PostHeader.propTypes = {
|
||||
post: React.PropTypes.object.isRequired,
|
||||
user: React.PropTypes.object,
|
||||
currentUser: React.PropTypes.object.isRequired,
|
||||
commentCount: React.PropTypes.number.isRequired,
|
||||
isLastComment: React.PropTypes.bool.isRequired,
|
||||
handleCommentClick: React.PropTypes.func.isRequired,
|
||||
sameUser: React.PropTypes.bool.isRequired
|
||||
};
|
||||
83
webapp/components/post_image.jsx
Обычный файл
83
webapp/components/post_image.jsx
Обычный файл
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostImageEmbed extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleLoadComplete = this.handleLoadComplete.bind(this);
|
||||
this.handleLoadError = this.handleLoadError.bind(this);
|
||||
|
||||
this.state = {
|
||||
loaded: false,
|
||||
errored: false
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.loadImg(this.props.link);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.link !== this.props.link) {
|
||||
this.setState({
|
||||
loaded: false,
|
||||
errored: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!this.state.loaded && prevProps.link !== this.props.link) {
|
||||
this.loadImg(this.props.link);
|
||||
}
|
||||
}
|
||||
|
||||
loadImg(src) {
|
||||
const img = new Image();
|
||||
img.onload = this.handleLoadComplete;
|
||||
img.onerror = this.handleLoadError;
|
||||
img.src = src;
|
||||
}
|
||||
|
||||
handleLoadComplete() {
|
||||
this.setState({
|
||||
loaded: true
|
||||
});
|
||||
}
|
||||
|
||||
handleLoadError() {
|
||||
this.setState({
|
||||
errored: true,
|
||||
loaded: true
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.errored) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!this.state.loaded) {
|
||||
return (
|
||||
<img
|
||||
className='img-div placeholder'
|
||||
height='500px'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<img
|
||||
className='img-div'
|
||||
src={this.props.link}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostImageEmbed.propTypes = {
|
||||
link: React.PropTypes.string.isRequired
|
||||
};
|
||||
252
webapp/components/post_info.jsx
Обычный файл
252
webapp/components/post_info.jsx
Обычный файл
@@ -0,0 +1,252 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import TimeSince from './time_since.jsx';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
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 = this.props.currentUser.id === post.user_id;
|
||||
var isAdmin = Utils.isAdmin(this.props.currentUser.roles);
|
||||
|
||||
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING || Utils.isPostEphemeral(post)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var type = 'Post';
|
||||
if (post.root_id && post.root_id.length > 0) {
|
||||
type = 'Comment';
|
||||
}
|
||||
|
||||
var dropdownContents = [];
|
||||
var dataComments = 0;
|
||||
if (type === 'Post') {
|
||||
dataComments = this.props.commentCount;
|
||||
}
|
||||
|
||||
if (this.props.allowReply === 'true') {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='replyLink'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
className='link__reply theme'
|
||||
href='#'
|
||||
onClick={this.props.handleCommentClick}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='post_info.reply'
|
||||
defaultMessage='Reply'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (!Utils.isMobile()) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='copyLink'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.handlePermalink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='post_info.permalink'
|
||||
defaultMessage='Permalink'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (isOwner || isAdmin) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='deletePost'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
role='menuitem'
|
||||
onClick={() => GlobalActions.showDeletePostModal(post, dataComments)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='post_info.del'
|
||||
defaultMessage='Delete'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (isOwner) {
|
||||
dropdownContents.push(
|
||||
<li
|
||||
key='editPost'
|
||||
role='presentation'
|
||||
>
|
||||
<a
|
||||
href='#'
|
||||
role='menuitem'
|
||||
data-toggle='modal'
|
||||
data-target='#edit_post'
|
||||
data-refocusid='#post_textbox'
|
||||
data-title={type}
|
||||
data-message={post.message}
|
||||
data-postid={post.id}
|
||||
data-channelid={post.channel_id}
|
||||
data-comments={dataComments}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='post_info.edit'
|
||||
defaultMessage='Edit'
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
if (dropdownContents.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<a
|
||||
href='#'
|
||||
className='dropdown-toggle post__dropdown theme'
|
||||
type='button'
|
||||
data-toggle='dropdown'
|
||||
aria-expanded='false'
|
||||
onClick={this.dropdownPosition}
|
||||
/>
|
||||
<ul
|
||||
className='dropdown-menu'
|
||||
role='menu'
|
||||
>
|
||||
{dropdownContents}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handlePermalink(e) {
|
||||
e.preventDefault();
|
||||
GlobalActions.showGetPostLinkModal(this.props.post);
|
||||
}
|
||||
|
||||
removePost() {
|
||||
GlobalActions.emitRemovePost(this.props.post);
|
||||
}
|
||||
createRemovePostButton(post) {
|
||||
if (!Utils.isPostEphemeral(post)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
href='#'
|
||||
className='post__remove theme'
|
||||
type='button'
|
||||
onClick={this.removePost}
|
||||
>
|
||||
{'×'}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
render() {
|
||||
var post = this.props.post;
|
||||
var comments = '';
|
||||
var showCommentClass = '';
|
||||
var commentCountText = this.props.commentCount;
|
||||
|
||||
if (this.props.commentCount >= 1) {
|
||||
showCommentClass = ' icon--show';
|
||||
} else {
|
||||
commentCountText = '';
|
||||
}
|
||||
|
||||
if (post.state !== Constants.POST_FAILED && post.state !== Constants.POST_LOADING && !Utils.isPostEphemeral(post)) {
|
||||
comments = (
|
||||
<a
|
||||
href='#'
|
||||
className={'comment-icon__container' + showCommentClass}
|
||||
onClick={this.props.handleCommentClick}
|
||||
>
|
||||
<span
|
||||
className='comment-icon'
|
||||
dangerouslySetInnerHTML={{__html: Constants.REPLY_ICON}}
|
||||
/>
|
||||
{commentCountText}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
var dropdown = this.createDropdown();
|
||||
|
||||
return (
|
||||
<ul className='post__header post__header--info'>
|
||||
<li className='col'>
|
||||
<TimeSince
|
||||
eventTime={post.create_at}
|
||||
sameUser={this.props.sameUser}
|
||||
/>
|
||||
</li>
|
||||
<li className='col col__reply'>
|
||||
<div
|
||||
className='dropdown'
|
||||
ref='dotMenu'
|
||||
>
|
||||
{dropdown}
|
||||
</div>
|
||||
{comments}
|
||||
{this.createRemovePostButton(post)}
|
||||
</li>
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostInfo.defaultProps = {
|
||||
post: null,
|
||||
commentCount: 0,
|
||||
isLastComment: false,
|
||||
allowReply: false,
|
||||
sameUser: false
|
||||
};
|
||||
PostInfo.propTypes = {
|
||||
post: React.PropTypes.object.isRequired,
|
||||
commentCount: React.PropTypes.number.isRequired,
|
||||
isLastComment: React.PropTypes.bool.isRequired,
|
||||
allowReply: React.PropTypes.string.isRequired,
|
||||
handleCommentClick: React.PropTypes.func.isRequired,
|
||||
sameUser: React.PropTypes.bool.isRequired,
|
||||
currentUser: React.PropTypes.object.isRequired
|
||||
};
|
||||
608
webapp/components/posts_view.jsx
Обычный файл
608
webapp/components/posts_view.jsx
Обычный файл
@@ -0,0 +1,608 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import $ from 'jquery';
|
||||
import ReactDOM from 'react-dom';
|
||||
import PreferenceStore from 'stores/preference_store.jsx';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Post from './post.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import DelayedAction from 'utils/delayed_action.jsx';
|
||||
|
||||
import {FormattedDate, FormattedMessage} from 'react-intl';
|
||||
|
||||
const Preferences = Constants.Preferences;
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostsView extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.updateState = this.updateState.bind(this);
|
||||
this.handleScroll = this.handleScroll.bind(this);
|
||||
this.handleScrollStop = this.handleScrollStop.bind(this);
|
||||
this.isAtBottom = this.isAtBottom.bind(this);
|
||||
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
|
||||
this.loadMorePostsBottom = this.loadMorePostsBottom.bind(this);
|
||||
this.createPosts = this.createPosts.bind(this);
|
||||
this.updateScrolling = this.updateScrolling.bind(this);
|
||||
this.handleResize = this.handleResize.bind(this);
|
||||
this.scrollToBottom = this.scrollToBottom.bind(this);
|
||||
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
|
||||
|
||||
this.jumpToPostNode = null;
|
||||
this.wasAtBottom = true;
|
||||
this.scrollHeight = 0;
|
||||
|
||||
this.scrollStopAction = new DelayedAction(this.handleScrollStop);
|
||||
|
||||
this.state = {
|
||||
displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false'),
|
||||
isScrolling: false,
|
||||
topPostId: null
|
||||
};
|
||||
}
|
||||
static get SCROLL_TYPE_FREE() {
|
||||
return 1;
|
||||
}
|
||||
static get SCROLL_TYPE_BOTTOM() {
|
||||
return 2;
|
||||
}
|
||||
static get SCROLL_TYPE_SIDEBAR_OPEN() {
|
||||
return 3;
|
||||
}
|
||||
static get SCROLL_TYPE_NEW_MESSAGE() {
|
||||
return 4;
|
||||
}
|
||||
static get SCROLL_TYPE_POST() {
|
||||
return 5;
|
||||
}
|
||||
updateState() {
|
||||
this.setState({displayNameType: PreferenceStore.get(Preferences.CATEGORY_DISPLAY_SETTINGS, 'name_format', 'false')});
|
||||
}
|
||||
isAtBottom() {
|
||||
// consider the view to be at the bottom if it's within this many pixels of the bottom
|
||||
const atBottomMargin = 10;
|
||||
|
||||
return this.refs.postlist.clientHeight + this.refs.postlist.scrollTop >= this.refs.postlist.scrollHeight - atBottomMargin;
|
||||
}
|
||||
handleScroll() {
|
||||
// HACK FOR RHS -- REMOVE WHEN RHS DIES
|
||||
const childNodes = this.refs.postlistcontent.childNodes;
|
||||
for (let i = 0; i < childNodes.length; i++) {
|
||||
// If the node is 1/3 down the page
|
||||
if (childNodes[i].offsetTop > (this.refs.postlist.scrollTop + (this.refs.postlist.offsetHeight / 3))) {
|
||||
this.jumpToPostNode = childNodes[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.wasAtBottom = this.isAtBottom();
|
||||
if (!this.jumpToPostNode && childNodes.length > 0) {
|
||||
this.jumpToPostNode = childNodes[childNodes.length - 1];
|
||||
}
|
||||
|
||||
// --- --------
|
||||
|
||||
this.props.postViewScrolled(this.isAtBottom());
|
||||
this.prevScrollHeight = this.refs.postlist.scrollHeight;
|
||||
this.prevOffsetTop = this.jumpToPostNode.offsetTop;
|
||||
|
||||
this.updateFloatingTimestamp();
|
||||
|
||||
if (!this.state.isScrolling) {
|
||||
this.setState({
|
||||
isScrolling: true
|
||||
});
|
||||
}
|
||||
|
||||
this.scrollStopAction.fireAfter(2000);
|
||||
}
|
||||
handleScrollStop() {
|
||||
this.setState({
|
||||
isScrolling: false
|
||||
});
|
||||
}
|
||||
updateFloatingTimestamp() {
|
||||
// skip this in non-mobile view since that's when the timestamp is visible
|
||||
if ($(window).width() > 768) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.postList) {
|
||||
// iterate through posts starting at the bottom since users are more likely to be viewing newer posts
|
||||
for (let i = 0; i < this.props.postList.order.length; i++) {
|
||||
const id = this.props.postList.order[i];
|
||||
const element = ReactDOM.findDOMNode(this.refs[id]);
|
||||
|
||||
if (!element || element.offsetTop + element.clientHeight <= this.refs.postlist.scrollTop) {
|
||||
// this post is off the top of the screen so the last one is at the top of the screen
|
||||
let topPostId;
|
||||
|
||||
if (i > 0) {
|
||||
topPostId = this.props.postList.order[i - 1];
|
||||
} else {
|
||||
// the first post we look at should always be on the screen, but handle that case anyway
|
||||
topPostId = id;
|
||||
}
|
||||
|
||||
if (topPostId !== this.state.topPostId) {
|
||||
this.setState({
|
||||
topPostId
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
loadMorePostsTop() {
|
||||
this.props.loadMorePostsTopClicked();
|
||||
}
|
||||
loadMorePostsBottom() {
|
||||
this.props.loadMorePostsBottomClicked();
|
||||
}
|
||||
createPosts(posts, order) {
|
||||
const postCtls = [];
|
||||
let previousPostDay = new Date(0);
|
||||
const userId = this.props.currentUser.id;
|
||||
const profiles = this.props.profiles || {};
|
||||
|
||||
let renderedLastViewed = false;
|
||||
|
||||
for (let i = order.length - 1; i >= 0; i--) {
|
||||
const post = posts[order[i]];
|
||||
const parentPost = posts[post.parent_id];
|
||||
const prevPost = posts[order[i + 1]];
|
||||
const postUserId = Utils.isSystemMessage(post) ? '' : post.user_id;
|
||||
|
||||
// If the post is a comment whose parent has been deleted, don't add it to the list.
|
||||
if (parentPost && parentPost.state === Constants.POST_DELETED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let sameUser = false;
|
||||
let sameRoot = false;
|
||||
let hideProfilePic = false;
|
||||
|
||||
if (prevPost) {
|
||||
const postIsComment = Utils.isComment(post);
|
||||
const prevPostIsComment = Utils.isComment(prevPost);
|
||||
const postFromWebhook = Boolean(post.props && post.props.from_webhook);
|
||||
const prevPostFromWebhook = Boolean(prevPost.props && prevPost.props.from_webhook);
|
||||
const prevPostUserId = Utils.isSystemMessage(prevPost) ? '' : prevPost.user_id;
|
||||
let prevWebhookName = '';
|
||||
if (prevPost.props && prevPost.props.override_username) {
|
||||
prevWebhookName = prevPost.props.override_username;
|
||||
}
|
||||
let curWebhookName = '';
|
||||
if (post.props && post.props.override_username) {
|
||||
curWebhookName = post.props.override_username;
|
||||
}
|
||||
|
||||
// consider posts from the same user if:
|
||||
// the previous post was made by the same user as the current post,
|
||||
// the previous post was made within 5 minutes of the current post,
|
||||
// the previous post and current post are both from webhooks or both not,
|
||||
// the previous post and current post have the same webhook usernames
|
||||
if (prevPostUserId === postUserId &&
|
||||
post.create_at - prevPost.create_at <= 1000 * 60 * 5 &&
|
||||
postFromWebhook === prevPostFromWebhook &&
|
||||
prevWebhookName === curWebhookName) {
|
||||
sameUser = true;
|
||||
}
|
||||
|
||||
// consider posts from the same root if:
|
||||
// the current post is a comment,
|
||||
// the current post has the same root as the previous post
|
||||
if (postIsComment && (prevPost.id === post.root_id || prevPost.root_id === post.root_id)) {
|
||||
sameRoot = true;
|
||||
}
|
||||
|
||||
// consider posts from the same root if:
|
||||
// the current post is not a comment,
|
||||
// the previous post is not a comment,
|
||||
// the previous post is from the same user
|
||||
if (!postIsComment && !prevPostIsComment && sameUser) {
|
||||
sameRoot = true;
|
||||
}
|
||||
|
||||
// hide the profile pic if:
|
||||
// the previous post was made by the same user as the current post,
|
||||
// the previous post is not a comment,
|
||||
// the current post is not a comment,
|
||||
// the previous post and current post are both from webhooks or both not,
|
||||
// the previous post and current post have the same webhook usernames
|
||||
if (prevPostUserId === postUserId &&
|
||||
!prevPostIsComment &&
|
||||
!postIsComment &&
|
||||
postFromWebhook === prevPostFromWebhook &&
|
||||
prevWebhookName === curWebhookName) {
|
||||
hideProfilePic = true;
|
||||
}
|
||||
}
|
||||
|
||||
// check if it's the last comment in a consecutive string of comments on the same post
|
||||
// it is the last comment if it is last post in the channel or the next post has a different root post
|
||||
const isLastComment = Utils.isComment(post) && (i === 0 || posts[order[i - 1]].root_id !== post.root_id);
|
||||
|
||||
const keyPrefix = post.id ? post.id : i;
|
||||
|
||||
const shouldHighlight = this.props.postsToHighlight && this.props.postsToHighlight.hasOwnProperty(post.id);
|
||||
|
||||
let profile;
|
||||
if (this.props.currentUser.id === post.user_id) {
|
||||
profile = this.props.currentUser;
|
||||
} else {
|
||||
profile = profiles[post.user_id];
|
||||
}
|
||||
|
||||
const postCtl = (
|
||||
<Post
|
||||
key={keyPrefix + 'postKey'}
|
||||
ref={post.id}
|
||||
sameUser={sameUser}
|
||||
sameRoot={sameRoot}
|
||||
post={post}
|
||||
parentPost={parentPost}
|
||||
posts={posts}
|
||||
hideProfilePic={hideProfilePic}
|
||||
isLastComment={isLastComment}
|
||||
shouldHighlight={shouldHighlight}
|
||||
onClick={() => GlobalActions.emitPostFocusEvent(post.id)} //eslint-disable-line no-loop-func
|
||||
displayNameType={this.state.displayNameType}
|
||||
user={profile}
|
||||
currentUser={this.props.currentUser}
|
||||
/>
|
||||
);
|
||||
|
||||
const currentPostDay = Utils.getDateForUnixTicks(post.create_at);
|
||||
if (currentPostDay.toDateString() !== previousPostDay.toDateString()) {
|
||||
postCtls.push(
|
||||
<div
|
||||
key={currentPostDay.toDateString()}
|
||||
className='date-separator'
|
||||
>
|
||||
<hr className='separator__hr'/>
|
||||
<div className='separator__text'>
|
||||
<FormattedDate
|
||||
value={currentPostDay}
|
||||
weekday='short'
|
||||
month='short'
|
||||
day='2-digit'
|
||||
year='numeric'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (postUserId !== userId &&
|
||||
this.props.messageSeparatorTime !== 0 &&
|
||||
post.create_at > this.props.messageSeparatorTime &&
|
||||
!renderedLastViewed) {
|
||||
renderedLastViewed = true;
|
||||
|
||||
// Temporary fix to solve ie11 rendering issue
|
||||
let newSeparatorId = '';
|
||||
if (!Utils.isBrowserIE()) {
|
||||
newSeparatorId = 'new_message_' + post.id;
|
||||
}
|
||||
postCtls.push(
|
||||
<div
|
||||
id={newSeparatorId}
|
||||
key='unviewed'
|
||||
ref='newMessageSeparator'
|
||||
className='new-separator'
|
||||
>
|
||||
<hr
|
||||
className='separator__hr'
|
||||
/>
|
||||
<div className='separator__text'>
|
||||
<FormattedMessage
|
||||
id='posts_view.newMsg'
|
||||
defaultMessage='New Messages'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
postCtls.push(postCtl);
|
||||
previousPostDay = currentPostDay;
|
||||
}
|
||||
|
||||
return postCtls;
|
||||
}
|
||||
updateScrolling() {
|
||||
if (this.props.scrollType === PostsView.SCROLL_TYPE_BOTTOM) {
|
||||
this.scrollToBottom();
|
||||
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_NEW_MESSAGE) {
|
||||
window.requestAnimationFrame(() => {
|
||||
// If separator exists scroll to it. Otherwise scroll to bottom.
|
||||
if (this.refs.newMessageSeparator) {
|
||||
var objDiv = this.refs.postlist;
|
||||
objDiv.scrollTop = this.refs.newMessageSeparator.offsetTop; //scrolls node to top of Div
|
||||
} else if (this.refs.postlist) {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
}
|
||||
});
|
||||
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_POST && this.props.scrollPostId) {
|
||||
window.requestAnimationFrame(() => {
|
||||
const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
|
||||
if (postNode == null) {
|
||||
return;
|
||||
}
|
||||
postNode.scrollIntoView();
|
||||
if (this.refs.postlist.scrollTop === postNode.offsetTop) {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
|
||||
} else {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - postNode.offsetTop);
|
||||
}
|
||||
});
|
||||
} else if (this.props.scrollType === PostsView.SCROLL_TYPE_SIDEBAR_OPEN) {
|
||||
// If we are at the bottom then stay there
|
||||
if (this.wasAtBottom) {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
} else {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.jumpToPostNode.scrollIntoView();
|
||||
if (this.refs.postlist.scrollTop === this.jumpToPostNode.offsetTop) {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3);
|
||||
} else {
|
||||
this.refs.postlist.scrollTop -= (this.refs.postlist.offsetHeight / 3) + (this.refs.postlist.scrollTop - this.jumpToPostNode.offsetTop);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (this.refs.postlist.scrollHeight !== this.prevScrollHeight) {
|
||||
window.requestAnimationFrame(() => {
|
||||
// Only need to jump if we added posts to the top.
|
||||
if (this.jumpToPostNode && (this.jumpToPostNode.offsetTop !== this.prevOffsetTop)) {
|
||||
this.refs.postlist.scrollTop += (this.refs.postlist.scrollHeight - this.prevScrollHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
handleResize() {
|
||||
this.updateScrolling();
|
||||
}
|
||||
scrollToBottom() {
|
||||
window.requestAnimationFrame(() => {
|
||||
this.refs.postlist.scrollTop = this.refs.postlist.scrollHeight;
|
||||
});
|
||||
}
|
||||
scrollToBottomAnimated() {
|
||||
var postList = $(this.refs.postlist);
|
||||
postList.animate({scrollTop: this.refs.postlist.scrollHeight}, '500');
|
||||
}
|
||||
componentDidMount() {
|
||||
if (this.props.postList != null) {
|
||||
this.updateScrolling();
|
||||
}
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
componentDidUpdate() {
|
||||
if (this.props.postList != null) {
|
||||
this.updateScrolling();
|
||||
}
|
||||
}
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (!this.props.isActive && nextProps.isActive) {
|
||||
this.updateState();
|
||||
PreferenceStore.addChangeListener(this.updateState);
|
||||
} else if (this.props.isActive && !nextProps.isActive) {
|
||||
PreferenceStore.removeChangeListener(this.updateState);
|
||||
}
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (this.props.isActive !== nextProps.isActive) {
|
||||
return true;
|
||||
}
|
||||
if (this.props.postList !== nextProps.postList) {
|
||||
return true;
|
||||
}
|
||||
if (this.props.scrollPostId !== nextProps.scrollPostId) {
|
||||
return true;
|
||||
}
|
||||
if (this.props.scrollType !== nextProps.scrollType && nextProps.scrollType !== PostsView.SCROLL_TYPE_FREE) {
|
||||
return true;
|
||||
}
|
||||
if (this.props.messageSeparatorTime !== nextProps.messageSeparatorTime) {
|
||||
return true;
|
||||
}
|
||||
if (!Utils.areObjectsEqual(this.props.postList, nextProps.postList)) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.displayNameType !== this.state.displayNameType) {
|
||||
return true;
|
||||
}
|
||||
if (this.state.topPostId !== nextState.topPostId) {
|
||||
return true;
|
||||
}
|
||||
if (this.state.isScrolling !== nextState.isScrolling) {
|
||||
return true;
|
||||
}
|
||||
if (!Utils.areObjectsEqual(this.props.profiles, nextProps.profiles)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
render() {
|
||||
let posts = [];
|
||||
let order = [];
|
||||
let moreMessagesTop;
|
||||
let moreMessagesBottom;
|
||||
let postElements;
|
||||
let activeClass = 'inactive';
|
||||
if (this.props.postList != null) {
|
||||
posts = this.props.postList.posts;
|
||||
order = this.props.postList.order;
|
||||
|
||||
// Create intro message or top loadmore link
|
||||
if (this.props.showMoreMessagesTop) {
|
||||
moreMessagesTop = (
|
||||
<a
|
||||
ref='loadmoretop'
|
||||
className='more-messages-text theme'
|
||||
href='#'
|
||||
onClick={this.loadMorePostsTop}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='posts_view.loadMore'
|
||||
defaultMessage='Load more messages'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
moreMessagesTop = this.props.introText;
|
||||
}
|
||||
|
||||
// Give option to load more posts at bottom if nessisary
|
||||
if (this.props.showMoreMessagesBottom) {
|
||||
moreMessagesBottom = (
|
||||
<a
|
||||
ref='loadmorebottom'
|
||||
className='more-messages-text theme'
|
||||
href='#'
|
||||
onClick={this.loadMorePostsBottom}
|
||||
>
|
||||
<FormattedMessage id='posts_view.loadMore'/>
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
moreMessagesBottom = null;
|
||||
}
|
||||
|
||||
// Create post elements
|
||||
postElements = this.createPosts(posts, order);
|
||||
|
||||
// Show ourselves if we are marked active
|
||||
if (this.props.isActive) {
|
||||
activeClass = '';
|
||||
}
|
||||
}
|
||||
|
||||
let topPost = null;
|
||||
if (this.state.topPostId) {
|
||||
topPost = this.props.postList.posts[this.state.topPostId];
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={activeClass}>
|
||||
<FloatingTimestamp
|
||||
isScrolling={this.state.isScrolling}
|
||||
post={topPost}
|
||||
/>
|
||||
<ScrollToBottomArrows
|
||||
isScrolling={this.state.isScrolling}
|
||||
atBottom={this.wasAtBottom}
|
||||
onClick={this.scrollToBottomAnimated}
|
||||
/>
|
||||
<div
|
||||
ref='postlist'
|
||||
className='post-list-holder-by-time'
|
||||
onScroll={this.handleScroll}
|
||||
>
|
||||
<div className='post-list__table'>
|
||||
<div
|
||||
ref='postlistcontent'
|
||||
className='post-list__content'
|
||||
>
|
||||
{moreMessagesTop}
|
||||
{postElements}
|
||||
{moreMessagesBottom}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
PostsView.defaultProps = {
|
||||
};
|
||||
|
||||
PostsView.propTypes = {
|
||||
isActive: React.PropTypes.bool,
|
||||
postList: React.PropTypes.object,
|
||||
profiles: React.PropTypes.object.isRequired,
|
||||
scrollPostId: React.PropTypes.string,
|
||||
scrollType: React.PropTypes.number,
|
||||
postViewScrolled: React.PropTypes.func.isRequired,
|
||||
loadMorePostsTopClicked: React.PropTypes.func.isRequired,
|
||||
loadMorePostsBottomClicked: React.PropTypes.func.isRequired,
|
||||
showMoreMessagesTop: React.PropTypes.bool,
|
||||
showMoreMessagesBottom: React.PropTypes.bool,
|
||||
introText: React.PropTypes.element,
|
||||
messageSeparatorTime: React.PropTypes.number,
|
||||
postsToHighlight: React.PropTypes.object,
|
||||
currentUser: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
function FloatingTimestamp({isScrolling, post}) {
|
||||
// only show on mobile
|
||||
if ($(window).width() > 768) {
|
||||
return <noscript/>;
|
||||
}
|
||||
|
||||
if (!post) {
|
||||
return <noscript/>;
|
||||
}
|
||||
|
||||
const dateString = (
|
||||
<FormattedDate
|
||||
value={post.create_at}
|
||||
weekday='short'
|
||||
day='2-digit'
|
||||
month='short'
|
||||
year='numeric'
|
||||
/>
|
||||
);
|
||||
|
||||
let className = 'post-list__timestamp';
|
||||
if (isScrolling) {
|
||||
className += ' scrolling';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<span>{dateString}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FloatingTimestamp.propTypes = {
|
||||
isScrolling: React.PropTypes.bool.isRequired,
|
||||
post: React.PropTypes.object
|
||||
};
|
||||
|
||||
function ScrollToBottomArrows({isScrolling, atBottom, onClick}) {
|
||||
// only show on mobile
|
||||
if ($(window).width() > 768) {
|
||||
return <noscript/>;
|
||||
}
|
||||
|
||||
let className = 'post-list__arrows';
|
||||
if (isScrolling && !atBottom) {
|
||||
className += ' scrolling';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{__html: Constants.SCROLL_BOTTOM_ICON}}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ScrollToBottomArrows.propTypes = {
|
||||
isScrolling: React.PropTypes.bool.isRequired,
|
||||
atBottom: React.PropTypes.bool.isRequired,
|
||||
onClick: React.PropTypes.func.isRequired
|
||||
};
|
||||
225
webapp/components/posts_view_container.jsx
Обычный файл
225
webapp/components/posts_view_container.jsx
Обычный файл
@@ -0,0 +1,225 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import PostsView from './posts_view.jsx';
|
||||
import LoadingScreen from './loading_screen.jsx';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import PostStore from 'stores/post_store.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import * as GlobalActions from 'action_creators/global_actions.jsx';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import {createChannelIntroMessage} from 'utils/channel_intro_messages.jsx';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export default class PostsViewContainer extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onChannelChange = this.onChannelChange.bind(this);
|
||||
this.onChannelLeave = this.onChannelLeave.bind(this);
|
||||
this.onPostsChange = this.onPostsChange.bind(this);
|
||||
this.onUserChange = this.onUserChange.bind(this);
|
||||
this.handlePostsViewScroll = this.handlePostsViewScroll.bind(this);
|
||||
this.loadMorePostsTop = this.loadMorePostsTop.bind(this);
|
||||
this.handlePostsViewJumpRequest = this.handlePostsViewJumpRequest.bind(this);
|
||||
|
||||
const currentChannelId = ChannelStore.getCurrentId();
|
||||
const state = {
|
||||
scrollType: PostsView.SCROLL_TYPE_BOTTOM,
|
||||
scrollPost: null,
|
||||
currentUser: UserStore.getCurrentUser()
|
||||
};
|
||||
if (currentChannelId) {
|
||||
Object.assign(state, {
|
||||
currentChannelIndex: 0,
|
||||
channels: [currentChannelId],
|
||||
postLists: [this.getChannelPosts(currentChannelId)],
|
||||
atTop: [PostStore.getVisibilityAtTop(currentChannelId)]
|
||||
});
|
||||
} else {
|
||||
Object.assign(state, {
|
||||
currentChannelIndex: null,
|
||||
channels: [],
|
||||
postLists: [],
|
||||
atTop: []
|
||||
});
|
||||
}
|
||||
|
||||
state.showInviteModal = false;
|
||||
this.state = state;
|
||||
}
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onChannelChange);
|
||||
ChannelStore.addLeaveListener(this.onChannelLeave);
|
||||
PostStore.addChangeListener(this.onPostsChange);
|
||||
PostStore.addPostsViewJumpListener(this.handlePostsViewJumpRequest);
|
||||
UserStore.addChangeListener(this.onUserChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onChannelChange);
|
||||
ChannelStore.removeLeaveListener(this.onChannelLeave);
|
||||
PostStore.removeChangeListener(this.onPostsChange);
|
||||
PostStore.removePostsViewJumpListener(this.handlePostsViewJumpRequest);
|
||||
UserStore.removeChangeListener(this.onUserChange);
|
||||
}
|
||||
onUserChange() {
|
||||
this.setState({currentUser: UserStore.getCurrentUser()});
|
||||
}
|
||||
handlePostsViewJumpRequest(type, post) {
|
||||
switch (type) {
|
||||
case Constants.PostsViewJumpTypes.BOTTOM:
|
||||
this.setState({scrollType: PostsView.SCROLL_TYPE_BOTTOM});
|
||||
break;
|
||||
case Constants.PostsViewJumpTypes.POST:
|
||||
this.setState({
|
||||
scrollType: PostsView.SCROLL_TYPE_POST,
|
||||
scrollPost: post
|
||||
});
|
||||
break;
|
||||
case Constants.PostsViewJumpTypes.SIDEBAR_OPEN:
|
||||
this.setState({scrollType: PostsView.SCROLL_TYPE_SIDEBAR_OPEN});
|
||||
break;
|
||||
}
|
||||
}
|
||||
onChannelChange() {
|
||||
const postLists = this.state.postLists.slice();
|
||||
const atTop = this.state.atTop.slice();
|
||||
const channels = this.state.channels.slice();
|
||||
const channelId = ChannelStore.getCurrentId();
|
||||
|
||||
// Has the channel really changed?
|
||||
if (channelId === channels[this.state.currentChannelIndex]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let lastViewed = Number.MAX_VALUE;
|
||||
const member = ChannelStore.getMember(channelId);
|
||||
if (member != null) {
|
||||
lastViewed = member.last_viewed_at;
|
||||
}
|
||||
|
||||
let newIndex = channels.indexOf(channelId);
|
||||
if (newIndex === -1) {
|
||||
newIndex = channels.length;
|
||||
channels.push(channelId);
|
||||
atTop[newIndex] = PostStore.getVisibilityAtTop(channelId);
|
||||
}
|
||||
|
||||
// make sure we have the latest posts from the store
|
||||
postLists[newIndex] = this.getChannelPosts(channelId);
|
||||
|
||||
this.setState({
|
||||
currentChannelIndex: newIndex,
|
||||
currentLastViewed: lastViewed,
|
||||
scrollType: PostsView.SCROLL_TYPE_NEW_MESSAGE,
|
||||
channels,
|
||||
postLists,
|
||||
atTop});
|
||||
}
|
||||
onChannelLeave(id) {
|
||||
const postLists = this.state.postLists.slice();
|
||||
const channels = this.state.channels.slice();
|
||||
const atTop = this.state.atTop.slice();
|
||||
const index = channels.indexOf(id);
|
||||
if (index !== -1) {
|
||||
postLists.splice(index, 1);
|
||||
channels.splice(index, 1);
|
||||
atTop.splice(index, 1);
|
||||
}
|
||||
this.setState({channels, postLists, atTop});
|
||||
}
|
||||
onPostsChange() {
|
||||
const channels = this.state.channels;
|
||||
const postLists = this.state.postLists.slice();
|
||||
const atTop = this.state.atTop.slice();
|
||||
const currentChannelId = channels[this.state.currentChannelIndex];
|
||||
const newPostsView = this.getChannelPosts(currentChannelId);
|
||||
|
||||
postLists[this.state.currentChannelIndex] = newPostsView;
|
||||
atTop[this.state.currentChannelIndex] = PostStore.getVisibilityAtTop(currentChannelId);
|
||||
this.setState({postLists, atTop});
|
||||
}
|
||||
getChannelPosts(id) {
|
||||
return PostStore.getVisiblePosts(id);
|
||||
}
|
||||
loadMorePostsTop() {
|
||||
GlobalActions.emitLoadMorePostsEvent();
|
||||
}
|
||||
handlePostsViewScroll(atBottom) {
|
||||
if (atBottom) {
|
||||
this.setState({scrollType: PostsView.SCROLL_TYPE_BOTTOM});
|
||||
} else {
|
||||
this.setState({scrollType: PostsView.SCROLL_TYPE_FREE});
|
||||
}
|
||||
}
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!Utils.areObjectsEqual(this.state, nextState)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Utils.areObjectsEqual(this.props, nextProps)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
render() {
|
||||
const postLists = this.state.postLists;
|
||||
const channels = this.state.channels;
|
||||
const currentChannelId = channels[this.state.currentChannelIndex];
|
||||
const channel = ChannelStore.get(currentChannelId);
|
||||
|
||||
if (!this.state.currentUser || !channel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const postListCtls = [];
|
||||
for (let i = 0; i < channels.length; i++) {
|
||||
const isActive = (channels[i] === currentChannelId);
|
||||
postListCtls.push(
|
||||
<PostsView
|
||||
key={'postsviewkey' + i}
|
||||
isActive={isActive}
|
||||
postList={postLists[i]}
|
||||
scrollType={this.state.scrollType}
|
||||
scrollPostId={this.state.scrollPost}
|
||||
postViewScrolled={this.handlePostsViewScroll}
|
||||
loadMorePostsTopClicked={this.loadMorePostsTop}
|
||||
loadMorePostsBottomClicked={() => {
|
||||
// Do Nothing
|
||||
}}
|
||||
showMoreMessagesTop={!this.state.atTop[this.state.currentChannelIndex]}
|
||||
showMoreMessagesBottom={false}
|
||||
introText={channel ? createChannelIntroMessage(channel) : null}
|
||||
messageSeparatorTime={this.state.currentLastViewed}
|
||||
profiles={this.props.profiles}
|
||||
currentUser={this.state.currentUser}
|
||||
/>
|
||||
);
|
||||
if (!postLists[i] && isActive) {
|
||||
postListCtls.push(
|
||||
<LoadingScreen
|
||||
position='absolute'
|
||||
key='loading'
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div id='post-list'>
|
||||
{postListCtls}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PostsViewContainer.propTypes = {
|
||||
profiles: React.PropTypes.object
|
||||
};
|
||||
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Ссылка в новой задаче
Block a user