Merge pull request #545 from mattermost/mm-2066

MM-2066 Refactoring for style guide
Этот коммит содержится в:
Corey Hulen
2015-09-02 12:41:28 -07:00
родитель 833f6b7df1 b9e16f41f1
Коммит f9dd82253c
15 изменённых файлов: 1704 добавлений и 1005 удалений

Просмотреть файл

@@ -4,48 +4,49 @@
var UserStore = require('../stores/user_store.jsx'); var UserStore = require('../stores/user_store.jsx');
var AsyncClient = require('../utils/async_client.jsx'); var AsyncClient = require('../utils/async_client.jsx');
var LoadingScreen = require('./loading_screen.jsx'); var LoadingScreen = require('./loading_screen.jsx');
var utils = require('../utils/utils.jsx'); var Utils = require('../utils/utils.jsx');
function getStateFromStoresForAudits() { export default class AccessHistoryModal extends React.Component {
return { constructor(props) {
audits: UserStore.getAudits() super(props);
};
}
module.exports = React.createClass({ this.onAuditChange = this.onAuditChange.bind(this);
displayName: 'AccessHistoryModal', this.handleMoreInfo = this.handleMoreInfo.bind(this);
componentDidMount: function() {
UserStore.addAuditsChangeListener(this.onListenerChange); this.state = this.getStateFromStoresForAudits();
$(this.refs.modal.getDOMNode()).on('shown.bs.modal', function() { this.state.moreInfo = [];
}
getStateFromStoresForAudits() {
return {
audits: UserStore.getAudits()
};
}
componentDidMount() {
UserStore.addAuditsChangeListener(this.onAuditChange);
$(React.findDOMNode(this.refs.modal)).on('shown.bs.modal', function show() {
AsyncClient.getAudits(); AsyncClient.getAudits();
}); });
var self = this; $(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', function hide() {
$(this.refs.modal.getDOMNode()).on('hidden.bs.modal', function() {
$('#user_settings').modal('show'); $('#user_settings').modal('show');
self.setState({moreInfo: []}); this.setState({moreInfo: []});
}); }.bind(this));
}, }
componentWillUnmount: function() { componentWillUnmount() {
UserStore.removeAuditsChangeListener(this.onListenerChange); UserStore.removeAuditsChangeListener(this.onAuditChange);
}, }
onListenerChange: function() { onAuditChange() {
var newState = getStateFromStoresForAudits(); var newState = this.getStateFromStoresForAudits();
if (!utils.areStatesEqual(newState.audits, this.state.audits)) { if (!Utils.areStatesEqual(newState.audits, this.state.audits)) {
this.setState(newState); this.setState(newState);
} }
}, }
handleMoreInfo: function(index) { handleMoreInfo(index) {
var newMoreInfo = this.state.moreInfo; var newMoreInfo = this.state.moreInfo;
newMoreInfo[index] = true; newMoreInfo[index] = true;
this.setState({moreInfo: newMoreInfo}); this.setState({moreInfo: newMoreInfo});
}, }
getInitialState: function() { render() {
var initialState = getStateFromStoresForAudits();
initialState.moreInfo = [];
return initialState;
},
render: function() {
var accessList = []; var accessList = [];
var currentHistoryDate = null; var currentHistoryDate = null;
@@ -63,7 +64,16 @@ module.exports = React.createClass({
currentAudit.session_id = 'N/A (Login attempt)'; currentAudit.session_id = 'N/A (Login attempt)';
} }
var moreInfo = (<a href='#' className='theme' onClick={this.handleMoreInfo.bind(this, i)}>More info</a>); var moreInfo = (
<a
href='#'
className='theme'
onClick={this.handleMoreInfo.bind(this, i)}
>
More info
</a>
);
if (this.state.moreInfo[i]) { if (this.state.moreInfo[i]) {
moreInfo = ( moreInfo = (
<div> <div>
@@ -75,7 +85,7 @@ module.exports = React.createClass({
var divider = null; var divider = null;
if (i < this.state.audits.length - 1) { if (i < this.state.audits.length - 1) {
divider = (<div className='divider-light'></div>) divider = (<div className='divider-light'></div>);
} }
accessList[i] = ( accessList[i] = (
@@ -102,14 +112,36 @@ module.exports = React.createClass({
return ( return (
<div> <div>
<div className='modal fade' ref='modal' id='access-history' tabIndex='-1' role='dialog' aria-hidden='true'> <div
className='modal fade'
ref='modal'
id='access-history'
tabIndex='-1'
role='dialog'
aria-hidden='true'
>
<div className='modal-dialog modal-lg'> <div className='modal-dialog modal-lg'>
<div className='modal-content'> <div className='modal-content'>
<div className='modal-header'> <div className='modal-header'>
<button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>&times;</span></button> <button
<h4 className='modal-title' id='myModalLabel'>Access History</h4> type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>&times;</span>
</button>
<h4
className='modal-title'
id='myModalLabel'
>
Access History
</h4>
</div> </div>
<div ref='modalBody' className='modal-body'> <div
ref='modalBody'
className='modal-body'
>
{content} {content}
</div> </div>
</div> </div>
@@ -118,4 +150,4 @@ module.exports = React.createClass({
</div> </div>
); );
} }
}); }

Просмотреть файл

@@ -4,26 +4,29 @@
var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMin = require('./setting_item_min.jsx');
var SettingItemMax = require('./setting_item_max.jsx'); var SettingItemMax = require('./setting_item_max.jsx');
var utils = require('../utils/utils.jsx'); var Utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx'); var Client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx'); var UserStore = require('../stores/user_store.jsx');
var ChannelStore = require('../stores/channel_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx');
export default class ChannelNotifications extends React.Component { export default class ChannelNotifications extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.onListenerChange = this.onListenerChange.bind(this); this.onListenerChange = this.onListenerChange.bind(this);
this.updateSection = this.updateSection.bind(this); this.updateSection = this.updateSection.bind(this);
this.handleUpdate = this.handleUpdate.bind(this); this.handleUpdate = this.handleUpdate.bind(this);
this.handleRadioClick = this.handleRadioClick.bind(this); this.handleRadioClick = this.handleRadioClick.bind(this);
this.handleQuietToggle = this.handleQuietToggle.bind(this); this.handleQuietToggle = this.handleQuietToggle.bind(this);
this.createDesktopSection = this.createDesktopSection.bind(this);
this.createQuietSection = this.createQuietSection.bind(this);
this.state = {notifyLevel: '', title: '', channelId: '', activeSection: ''}; this.state = {notifyLevel: '', title: '', channelId: '', activeSection: ''};
} }
componentDidMount() { componentDidMount() {
ChannelStore.addChangeListener(this.onListenerChange); ChannelStore.addChangeListener(this.onListenerChange);
var self = this; $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function showModal(e) {
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function showModal(e) {
var button = e.relatedTarget; var button = e.relatedTarget;
var channelId = button.getAttribute('data-channelid'); var channelId = button.getAttribute('data-channelid');
@@ -34,8 +37,8 @@ export default class ChannelNotifications extends React.Component {
quietMode = true; quietMode = true;
} }
self.setState({notifyLevel: notifyLevel, quietMode: quietMode, title: button.getAttribute('data-title'), channelId: channelId}); this.setState({notifyLevel: notifyLevel, quietMode: quietMode, title: button.getAttribute('data-title'), channelId: channelId});
}); }.bind(this));
} }
componentWillUnmount() { componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange); ChannelStore.removeChangeListener(this.onListenerChange);
@@ -55,7 +58,7 @@ export default class ChannelNotifications extends React.Component {
newState.notifyLevel = notifyLevel; newState.notifyLevel = notifyLevel;
newState.quietMode = quietMode; newState.quietMode = quietMode;
if (!utils.areStatesEqual(this.state, newState)) { if (!Utils.areStatesEqual(this.state, newState)) {
this.setState(newState); this.setState(newState);
} }
} }
@@ -78,7 +81,7 @@ export default class ChannelNotifications extends React.Component {
return; return;
} }
client.updateNotifyLevel(data, Client.updateNotifyLevel(data,
function success() { function success() {
var member = ChannelStore.getMember(channelId); var member = ChannelStore.getMember(channelId);
member.notify_level = notifyLevel; member.notify_level = notifyLevel;
@@ -92,25 +95,15 @@ export default class ChannelNotifications extends React.Component {
} }
handleRadioClick(notifyLevel) { handleRadioClick(notifyLevel) {
this.setState({notifyLevel: notifyLevel, quietMode: false}); this.setState({notifyLevel: notifyLevel, quietMode: false});
this.refs.modal.getDOMNode().focus(); React.findDOMNode(this.refs.modal).focus();
} }
handleQuietToggle(quietMode) { handleQuietToggle(quietMode) {
this.setState({notifyLevel: 'none', quietMode: quietMode}); this.setState({notifyLevel: 'none', quietMode: quietMode});
this.refs.modal.getDOMNode().focus(); React.findDOMNode(this.refs.modal).focus();
} }
render() { createDesktopSection(serverError) {
var serverError = null;
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var self = this;
var describe = '';
var inputs = [];
var handleUpdateSection; var handleUpdateSection;
var desktopSection;
if (this.state.activeSection === 'desktop') { if (this.state.activeSection === 'desktop') {
var notifyActive = [false, false, false]; var notifyActive = [false, false, false];
if (this.state.notifyLevel === 'mention') { if (this.state.notifyLevel === 'mention') {
@@ -121,6 +114,8 @@ export default class ChannelNotifications extends React.Component {
notifyActive[2] = true; notifyActive[2] = true;
} }
var inputs = [];
inputs.push( inputs.push(
<div> <div>
<div className='radio'> <div className='radio'>
@@ -128,7 +123,7 @@ export default class ChannelNotifications extends React.Component {
<input <input
type='radio' type='radio'
checked={notifyActive[0]} checked={notifyActive[0]}
onChange={self.handleRadioClick.bind(this, 'all')} onChange={this.handleRadioClick.bind(this, 'all')}
> >
For all activity For all activity
</input> </input>
@@ -140,7 +135,7 @@ export default class ChannelNotifications extends React.Component {
<input <input
type='radio' type='radio'
checked={notifyActive[1]} checked={notifyActive[1]}
onChange={self.handleRadioClick.bind(this, 'mention')} onChange={this.handleRadioClick.bind(this, 'mention')}
> >
Only for mentions Only for mentions
</input> </input>
@@ -152,7 +147,7 @@ export default class ChannelNotifications extends React.Component {
<input <input
type='radio' type='radio'
checked={notifyActive[2]} checked={notifyActive[2]}
onChange={self.handleRadioClick.bind(this, 'none')} onChange={this.handleRadioClick.bind(this, 'none')}
> >
Never Never
</input> </input>
@@ -162,12 +157,12 @@ export default class ChannelNotifications extends React.Component {
); );
handleUpdateSection = function updateSection(e) { handleUpdateSection = function updateSection(e) {
self.updateSection(''); this.updateSection('');
self.onListenerChange(); this.onListenerChange();
e.preventDefault(); e.preventDefault();
}; }.bind(this);
desktopSection = ( return (
<SettingItemMax <SettingItemMax
title='Send desktop notifications' title='Send desktop notifications'
inputs={inputs} inputs={inputs}
@@ -176,30 +171,32 @@ export default class ChannelNotifications extends React.Component {
updateSection={handleUpdateSection} updateSection={handleUpdateSection}
/> />
); );
} else {
if (this.state.notifyLevel === 'mention') {
describe = 'Only for mentions';
} else if (this.state.notifyLevel === 'all') {
describe = 'For all activity';
} else {
describe = 'Never';
}
handleUpdateSection = function updateSection(e) {
self.updateSection('desktop');
e.preventDefault();
};
desktopSection = (
<SettingItemMin
title='Send desktop notifications'
describe={describe}
updateSection={handleUpdateSection}
/>
);
} }
var quietSection; var describe;
if (this.state.notifyLevel === 'mention') {
describe = 'Only for mentions';
} else if (this.state.notifyLevel === 'all') {
describe = 'For all activity';
} else {
describe = 'Never';
}
handleUpdateSection = function updateSection(e) {
this.updateSection('desktop');
e.preventDefault();
}.bind(this);
return (
<SettingItemMin
title='Send desktop notifications'
describe={describe}
updateSection={handleUpdateSection}
/>
);
}
createQuietSection(serverError) {
var handleUpdateSection;
if (this.state.activeSection === 'quiet') { if (this.state.activeSection === 'quiet') {
var quietActive = [false, false]; var quietActive = [false, false];
if (this.state.quietMode) { if (this.state.quietMode) {
@@ -208,6 +205,8 @@ export default class ChannelNotifications extends React.Component {
quietActive[1] = true; quietActive[1] = true;
} }
var inputs = [];
inputs.push( inputs.push(
<div> <div>
<div className='radio'> <div className='radio'>
@@ -215,7 +214,7 @@ export default class ChannelNotifications extends React.Component {
<input <input
type='radio' type='radio'
checked={quietActive[0]} checked={quietActive[0]}
onChange={self.handleQuietToggle.bind(this, true)} onChange={this.handleQuietToggle.bind(this, true)}
> >
On On
</input> </input>
@@ -227,7 +226,7 @@ export default class ChannelNotifications extends React.Component {
<input <input
type='radio' type='radio'
checked={quietActive[1]} checked={quietActive[1]}
onChange={self.handleQuietToggle.bind(this, false)} onChange={this.handleQuietToggle.bind(this, false)}
> >
Off Off
</input> </input>
@@ -245,12 +244,12 @@ export default class ChannelNotifications extends React.Component {
); );
handleUpdateSection = function updateSection(e) { handleUpdateSection = function updateSection(e) {
self.updateSection(''); this.updateSection('');
self.onListenerChange(); this.onListenerChange();
e.preventDefault(); e.preventDefault();
}; }.bind(this);
quietSection = ( return (
<SettingItemMax <SettingItemMax
title='Quiet mode' title='Quiet mode'
inputs={inputs} inputs={inputs}
@@ -259,27 +258,38 @@ export default class ChannelNotifications extends React.Component {
updateSection={handleUpdateSection} updateSection={handleUpdateSection}
/> />
); );
} else {
if (this.state.quietMode) {
describe = 'On';
} else {
describe = 'Off';
}
handleUpdateSection = function updateSection(e) {
self.updateSection('quiet');
e.preventDefault();
};
quietSection = (
<SettingItemMin
title='Quiet mode'
describe={describe}
updateSection={handleUpdateSection}
/>
);
} }
var describe;
if (this.state.quietMode) {
describe = 'On';
} else {
describe = 'Off';
}
handleUpdateSection = function updateSection(e) {
this.updateSection('quiet');
e.preventDefault();
}.bind(this);
return (
<SettingItemMin
title='Quiet mode'
describe={describe}
updateSection={handleUpdateSection}
/>
);
}
render() {
var serverError = null;
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var desktopSection = this.createDesktopSection(serverError);
var quietSection = this.createQuietSection(serverError);
return ( return (
<div <div
className='modal fade' className='modal fade'

Просмотреть файл

@@ -2,69 +2,114 @@
// See License.txt for license information. // See License.txt for license information.
var UserStore = require('../stores/user_store.jsx'); var UserStore = require('../stores/user_store.jsx');
var ZeroClipboardMixin = require('react-zeroclipboard-mixin');
ZeroClipboardMixin.ZeroClipboard.config({ export default class GetLinkModal extends React.Component {
swfPath: '../../static/flash/ZeroClipboard.swf' constructor(props) {
}); super(props);
module.exports = React.createClass({ this.handleClick = this.handleClick.bind(this);
displayName: 'GetLinkModal',
zeroclipboardElementsSelector: '[data-copy-btn]', this.state = {copiedLink: false};
mixins: [ZeroClipboardMixin], }
componentDidMount: function() { componentDidMount() {
var self = this;
if (this.refs.modal) { if (this.refs.modal) {
$(this.refs.modal.getDOMNode()).on('show.bs.modal', function(e) { $(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function show(e) {
var button = e.relatedTarget; var button = e.relatedTarget;
self.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')}); this.setState({title: $(button).attr('data-title'), value: $(button).attr('data-value')});
}); }.bind(this));
$(this.refs.modal.getDOMNode()).on('hide.bs.modal', function() { $(React.findDOMNode(this.refs.modal)).on('hide.bs.modal', function hide() {
self.setState({copiedLink: false}); this.setState({copiedLink: false});
}); }.bind(this));
} }
}, }
getInitialState: function() { handleClick() {
return {copiedLink: false}; var copyTextarea = $(React.findDOMNode(this.refs.textarea));
}, copyTextarea.select();
handleClick: function() {
this.setState({copiedLink: true}); try {
}, var successful = document.execCommand('copy');
render: function() { if (successful) {
this.setState({copiedLink: true});
} else {
this.setState({copiedLink: false});
}
} catch (err) {
this.setState({copiedLink: false});
}
}
render() {
var currentUser = UserStore.getCurrentUser(); var currentUser = UserStore.getCurrentUser();
var copyLinkConfirm = null; var copyLinkConfirm = null;
if (this.state.copiedLink) { if (this.state.copiedLink) {
copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className="fa fa-check"></i> Link copied to clipboard.</p>; copyLinkConfirm = <p className='alert alert-success copy-link-confirm'><i className='fa fa-check'></i> Link copied to clipboard.</p>;
} }
if (currentUser != null) { if (currentUser != null) {
return ( return (
<div className='modal fade' ref='modal' id='get_link' tabIndex='-1' role='dialog' aria-hidden='true'> <div
<div className='modal-dialog'> className='modal fade'
<div className='modal-content'> ref='modal'
<div className='modal-header'> id='get_link'
<button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>&times;</span></button> tabIndex='-1'
<h4 className='modal-title' id='myModalLabel'>{this.state.title} Link</h4> role='dialog'
aria-hidden='true'
>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<button
type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>&times;</span>
</button>
<h4
className='modal-title'
id='myModalLabel'
>
{this.state.title} Link
</h4>
</div>
<div className='modal-body'>
<p>
Send {strings.Team + 'mates'} the link below for them to sign-up to this {strings.Team} site.
<br /><br />
Be careful not to share this link publicly, since anyone with the link can join your {strings.Team}.
</p>
<textarea
className='form-control no-resize'
readOnly='true'
ref='textarea'
value={this.state.value}
/>
</div>
<div className='modal-footer'>
<button
type='button'
className='btn btn-default'
data-dismiss='modal'
>
Close
</button>
<button
data-copy-btn='true'
type='button'
className='btn btn-primary pull-left'
onClick={this.handleClick}
data-clipboard-text={this.state.value}
>
Copy Link
</button>
{copyLinkConfirm}
</div>
</div> </div>
<div className='modal-body'> </div>
<p>
Send {strings.Team + 'mates'} the link below for them to sign-up to this {strings.Team} site.
<br /><br />
Be careful not to share this link publicly, since anyone with the link can join your {strings.Team}.
</p>
<textarea className='form-control no-resize' readOnly='true' value={this.state.value}></textarea>
</div>
<div className='modal-footer'>
<button type='button' className='btn btn-default' data-dismiss='modal'>Close</button>
<button data-copy-btn='true' type='button' className='btn btn-primary pull-left' onClick={this.handleClick} data-clipboard-text={this.state.value}>Copy Link</button>
{copyLinkConfirm}
</div>
</div>
</div>
</div> </div>
); );
} }
return <div/>; return <div/>;
} }
}); }

Просмотреть файл

@@ -1,24 +1,31 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
module.exports = React.createClass({ export default class LoadingScreen extends React.Component {
displayName: "LoadingScreen", constructor(props) {
propTypes: { super(props);
position: React.PropTypes.oneOf(['absolute', 'fixed', 'relative', 'static', 'inherit']) this.state = {};
}, }
getDefaultProps: function() { render() {
return { position: 'relative' };
},
render: function() {
return ( return (
<div className="loading-screen" style={{position: this.props.position}}> <div
<div className="loading__content"> className='loading-screen'
style={{position: this.props.position}}
>
<div className='loading__content'>
<h3>Loading</h3> <h3>Loading</h3>
<div className="round round-1"></div> <div className='round round-1'></div>
<div className="round round-2"></div> <div className='round round-2'></div>
<div className="round round-3"></div> <div className='round round-3'></div>
</div> </div>
</div> </div>
); );
} }
}); }
LoadingScreen.defaultProps = {
position: 'relative'
};
LoadingScreen.propTypes = {
position: React.PropTypes.oneOf(['absolute', 'fixed', 'relative', 'static', 'inherit'])
};

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
var client = require('../utils/client.jsx'); var Client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx'); var AsyncClient = require('../utils/async_client.jsx');
var UserStore = require('../stores/user_store.jsx'); var UserStore = require('../stores/user_store.jsx');
var ChannelStore = require('../stores/channel_store.jsx'); var ChannelStore = require('../stores/channel_store.jsx');
@@ -13,22 +13,27 @@ var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes; var ActionTypes = Constants.ActionTypes;
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
function getStateFromStores() { export default class Navbar extends React.Component {
return { constructor(props) {
channel: ChannelStore.getCurrent(), super(props);
member: ChannelStore.getCurrentMember(),
users: ChannelStore.getCurrentExtraInfo().members
};
}
module.exports = React.createClass({ this.onChange = this.onChange.bind(this);
displayName: 'Navbar', this.handleLeave = this.handleLeave.bind(this);
propTypes: { this.createCollapseButtons = this.createCollapseButtons.bind(this);
teamDisplayName: React.PropTypes.string this.createDropdown = this.createDropdown.bind(this);
},
componentDidMount: function() { this.state = this.getStateFromStores();
ChannelStore.addChangeListener(this.onListenerChange); }
ChannelStore.addExtraInfoChangeListener(this.onListenerChange); getStateFromStores() {
return {
channel: ChannelStore.getCurrent(),
member: ChannelStore.getCurrentMember(),
users: ChannelStore.getCurrentExtraInfo().members
};
}
componentDidMount() {
ChannelStore.addChangeListener(this.onChange);
ChannelStore.addExtraInfoChangeListener(this.onChange);
$('.inner__wrap').click(this.hideSidebars); $('.inner__wrap').click(this.hideSidebars);
$('body').on('click.infopopover', function handlePopoverClick(e) { $('body').on('click.infopopover', function handlePopoverClick(e) {
@@ -36,15 +41,15 @@ module.exports = React.createClass({
$('.info-popover').popover('hide'); $('.info-popover').popover('hide');
} }
}); });
}, }
componentWillUnmount: function() { componentWillUnmount() {
ChannelStore.removeChangeListener(this.onListenerChange); ChannelStore.removeChangeListener(this.onChange);
}, }
handleSubmit: function(e) { handleSubmit(e) {
e.preventDefault(); e.preventDefault();
}, }
handleLeave: function() { handleLeave() {
client.leaveChannel(this.state.channel.id, Client.leaveChannel(this.state.channel.id,
function success() { function success() {
AsyncClient.getChannels(true); AsyncClient.getChannels(true);
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square'; window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/town-square';
@@ -53,8 +58,8 @@ module.exports = React.createClass({
AsyncClient.dispatchError(err, 'handleLeave'); AsyncClient.dispatchError(err, 'handleLeave');
} }
); );
}, }
hideSidebars: function(e) { hideSidebars(e) {
var windowWidth = $(window).outerWidth(); var windowWidth = $(window).outerWidth();
if (windowWidth <= 768) { if (windowWidth <= 768) {
AppDispatcher.handleServerAction({ AppDispatcher.handleServerAction({
@@ -74,32 +79,259 @@ module.exports = React.createClass({
$('.sidebar--menu').removeClass('move--left'); $('.sidebar--menu').removeClass('move--left');
} }
} }
}, }
toggleLeftSidebar: function() { toggleLeftSidebar() {
$('.inner__wrap').toggleClass('move--right'); $('.inner__wrap').toggleClass('move--right');
$('.sidebar--left').toggleClass('move--right'); $('.sidebar--left').toggleClass('move--right');
}, }
toggleRightSidebar: function() { toggleRightSidebar() {
$('.inner__wrap').toggleClass('move--left-small'); $('.inner__wrap').toggleClass('move--left-small');
$('.sidebar--menu').toggleClass('move--left'); $('.sidebar--menu').toggleClass('move--left');
}, }
onListenerChange: function() { onChange() {
this.setState(getStateFromStores()); this.setState(this.getStateFromStores());
$('#navbar .navbar-brand .description').popover({placement: 'bottom', trigger: 'click', html: true}); $('#navbar .navbar-brand .description').popover({placement: 'bottom', trigger: 'click', html: true});
}, }
getInitialState: function() { createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) {
return getStateFromStores(); if (channel) {
}, var viewInfoOption = (
render: function() { <li role='presentation'>
<a
role='menuitem'
data-toggle='modal'
data-target='#channel_info'
data-channelid={channel.id}
href='#'
>
View Info
</a>
</li>
);
var setChannelDescriptionOption = (
<li role='presentation'>
<a
role='menuitem'
href='#'
data-toggle='modal'
data-target='#edit_channel'
data-desc={channel.description}
data-title={channel.display_name}
data-channelid={channel.id}
>
Set Channel Description...
</a>
</li>
);
var addMembersOption;
var leaveChannelOption;
if (!isDirect && !ChannelStore.isDefault(channel)) {
addMembersOption = (
<li role='presentation'>
<a
role='menuitem'
data-toggle='modal'
data-target='#channel_invite'
href='#'
>
Add Members
</a>
</li>
);
leaveChannelOption = (
<li role='presentation'>
<a
role='menuitem'
href='#'
onClick={this.handleLeave}
>
Leave Channel
</a>
</li>
);
}
var manageMembersOption;
var renameChannelOption;
var deleteChannelOption;
if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) {
manageMembersOption = (
<li role='presentation'>
<a
role='menuitem'
data-toggle='modal'
data-target='#channel_members'
href='#'
>
Manage Members
</a>
</li>
);
renameChannelOption = (
<li role='presentation'>
<a
role='menuitem'
href='#'
data-toggle='modal'
data-target='#rename_channel'
data-display={channel.display_name}
data-name={channel.name}
data-channelid={channel.id}
>
Rename Channel...
</a>
</li>
);
deleteChannelOption = (
<li role='presentation'>
<a
role='menuitem'
href='#'
data-toggle='modal'
data-target='#delete_channel'
data-title={channel.display_name}
data-channelid={channel.id}
>
Delete Channel...
</a>
</li>
);
}
var notificationPreferenceOption;
if (!isDirect) {
notificationPreferenceOption = (
<li role='presentation'>
<a
role='menuitem'
href='#'
data-toggle='modal'
data-target='#channel_notifications'
data-title={channel.display_name}
data-channelid={channel.id}
>
Notification Preferences
</a>
</li>
);
}
return (
<div className='navbar-brand'>
<div className='dropdown'>
<div
data-toggle='popover'
data-content={popoverContent}
className='description info-popover'
/>
<a
href='#'
className='dropdown-toggle theme'
type='button'
id='channel_header_dropdown'
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'
aria-labelledby='channel_header_dropdown'
>
{viewInfoOption}
{addMembersOption}
{manageMembersOption}
{setChannelDescriptionOption}
{notificationPreferenceOption}
{renameChannelOption}
{deleteChannelOption}
{leaveChannelOption}
</ul>
</div>
</div>
);
}
return (
<div className='navbar-brand'>
<a
href='/'
className='heading'
>
{channelTitle}
</a>
</div>
);
}
createCollapseButtons(currentId) {
var buttons = [];
if (currentId == null) {
buttons.push(
<button
type='button'
className='navbar-toggle'
data-toggle='collapse'
data-target='#navbar-collapse-1'
>
<span className='sr-only'>Toggle sidebar</span>
<span className='icon-bar'></span>
<span className='icon-bar'></span>
<span className='icon-bar'></span>
</button>
);
} else {
buttons.push(
<button
type='button'
className='navbar-toggle'
data-toggle='collapse'
data-target='#sidebar-nav'
onClick={this.toggleLeftSidebar}
>
<span className='sr-only'>Toggle sidebar</span>
<span className='icon-bar'></span>
<span className='icon-bar'></span>
<span className='icon-bar'></span>
<NotifyCounts />
</button>
);
buttons.push(
<button
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() {
var currentId = UserStore.getCurrentId(); var currentId = UserStore.getCurrentId();
var popoverContent = ''; var channel = this.state.channel;
var channelTitle = this.props.teamDisplayName; var channelTitle = this.props.teamDisplayName;
var popoverContent;
var isAdmin = false; var isAdmin = false;
var isDirect = false; var isDirect = false;
var channel = this.state.channel;
if (channel) { if (channel) {
popoverContent = React.renderToString(<MessageWrapper message={channel.description} options={{singleline: true, noMentionHighlight: true}}/>); popoverContent = React.renderToString(
<MessageWrapper
message={channel.description}
options={{singleline: true, noMentionHighlight: true}}
/>
);
isAdmin = this.state.member.roles.indexOf('admin') > -1; isAdmin = this.state.member.roles.indexOf('admin') > -1;
if (channel.type === 'O') { if (channel.type === 'O') {
@@ -118,110 +350,46 @@ module.exports = React.createClass({
} }
if (channel.description.length === 0) { if (channel.description.length === 0) {
popoverContent = React.renderToString(<div>No channel description yet. <br /><a href='#' data-toggle='modal' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id} data-target='#edit_channel'>Click here</a> to add one.</div>); popoverContent = React.renderToString(
<div>
No channel description yet. <br/>
<a
href='#'
data-toggle='modal'
data-desc={channel.description}
data-title={channel.display_name}
data-channelid={channel.id}
data-target='#edit_channel'
>
Click here
</a> to add one.</div>
);
} }
} }
var navbarCollapseButton = null; var collapseButtons = this.createCollapseButtons(currentId);
if (currentId == null) {
navbarCollapseButton = (<button type='button' className='navbar-toggle' data-toggle='collapse' data-target='#navbar-collapse-1'>
<span className='sr-only'>Toggle sidebar</span>
<span className='icon-bar'></span>
<span className='icon-bar'></span>
<span className='icon-bar'></span>
</button>);
}
var sidebarCollapseButton = null; var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent);
if (currentId != null) {
sidebarCollapseButton = (<button type='button' className='navbar-toggle' data-toggle='collapse' data-target='#sidebar-nav' onClick={this.toggleLeftSidebar}>
<span className='sr-only'>Toggle sidebar</span>
<span className='icon-bar'></span>
<span className='icon-bar'></span>
<span className='icon-bar'></span>
<NotifyCounts />
</button>);
}
var rightSidebarCollapseButton = null;
if (currentId != null) {
rightSidebarCollapseButton = (<button 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>);
}
var channelMenuDropdown = null;
if (channel) {
var viewInfoOption = <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_info' data-channelid={channel.id} href='#'>View Info</a></li>
var addMembersOption = null;
if (!isDirect && !ChannelStore.isDefault(channel)) {
addMembersOption = <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_invite' href='#'>Add Members</a></li>;
}
var manageMembersOption = null;
if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) {
manageMembersOption = <li role='presentation'><a role='menuitem' data-toggle='modal' data-target='#channel_members' href='#'>Manage Members</a></li>;
}
var setChannelDescriptionOption = <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#edit_channel' data-desc={channel.description} data-title={channel.display_name} data-channelid={channel.id}>Set Channel Description...</a></li>;
var notificationPreferenceOption = null;
if (!isDirect) {
notificationPreferenceOption = <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#channel_notifications' data-title={channel.display_name} data-channelid={channel.id}>Notification Preferences</a></li>;
}
var renameChannelOption = null;
if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) {
renameChannelOption = <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#rename_channel' data-display={channel.display_name} data-name={channel.name} data-channelid={channel.id}>Rename Channel...</a></li>;
}
var deleteChannelOption = null;
if (!isDirect && isAdmin && !ChannelStore.isDefault(channel)) {
deleteChannelOption = <li role='presentation'><a role='menuitem' href='#' data-toggle='modal' data-target='#delete_channel' data-title={channel.display_name} data-channelid={channel.id}>Delete Channel...</a></li>;
}
var leaveChannelOption = null;
if (!isDirect && !ChannelStore.isDefault(channel)) {
leaveChannelOption = <li role='presentation'><a role='menuitem' href='#' onClick={this.handleLeave}>Leave Channel</a></li>;
}
channelMenuDropdown = (<div className='navbar-brand'>
<div className='dropdown'>
<div data-toggle='popover' data-content={popoverContent} className='description info-popover'></div>
<a href='#' className='dropdown-toggle theme' type='button' id='channel_header_dropdown' 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' aria-labelledby='channel_header_dropdown'>
{viewInfoOption}
{addMembersOption}
{manageMembersOption}
{setChannelDescriptionOption}
{notificationPreferenceOption}
{renameChannelOption}
{deleteChannelOption}
{leaveChannelOption}
</ul>
</div>
</div>);
} else {
channelMenuDropdown = (<div className='navbar-brand'>
<a href='/' className='heading'>{channelTitle}</a>
</div>);
}
return ( return (
<nav className='navbar navbar-default navbar-fixed-top' role='navigation'> <nav
className='navbar navbar-default navbar-fixed-top'
role='navigation'
>
<div className='container-fluid theme'> <div className='container-fluid theme'>
<div className='navbar-header'> <div className='navbar-header'>
{navbarCollapseButton} {collapseButtons}
{sidebarCollapseButton}
{rightSidebarCollapseButton}
{channelMenuDropdown} {channelMenuDropdown}
</div> </div>
</div> </div>
</nav> </nav>
); );
} }
}); }
Navbar.defaultProps = {
teamDisplayName: ''
};
Navbar.propTypes = {
teamDisplayName: React.PropTypes.string
};

Просмотреть файл

@@ -1,23 +1,42 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
var UserProfile = require( './user_profile.jsx' ); var UserProfile = require('./user_profile.jsx');
var PostInfo = require('./post_info.jsx'); var PostInfo = require('./post_info.jsx');
module.exports = React.createClass({ export default class PostHeader extends React.Component {
getInitialState: function() { constructor(props) {
return { }; super(props);
}, this.state = {};
render: function() { }
render() {
var post = this.props.post; var post = this.props.post;
return ( return (
<ul className="post-header post-header-post"> <ul className='post-header post-header-post'>
<li className="post-header-col post-header__name"><strong><UserProfile userId={post.user_id} /></strong></li> <li className='post-header-col post-header__name'><strong><UserProfile userId={post.user_id} /></strong></li>
<li className="post-info--hidden"> <li className='post-info--hidden'>
<PostInfo post={post} commentCount={this.props.commentCount} handleCommentClick={this.props.handleCommentClick} allowReply="true" isLastComment={this.props.isLastComment} /> <PostInfo
post={post}
commentCount={this.props.commentCount}
handleCommentClick={this.props.handleCommentClick}
allowReply='true'
isLastComment={this.props.isLastComment}
/>
</li> </li>
</ul> </ul>
); );
} }
}); }
PostHeader.defaultProps = {
post: null,
commentCount: 0,
isLastComment: false
};
PostHeader.propTypes = {
post: React.PropTypes.object,
commentCount: React.PropTypes.number,
isLastComment: React.PropTypes.bool,
handleCommentClick: React.PropTypes.func
};

Просмотреть файл

@@ -8,127 +8,137 @@ var SocketStore = require('../stores/socket_store.jsx');
var UserStore = require('../stores/user_store.jsx'); var UserStore = require('../stores/user_store.jsx');
var TeamStore = require('../stores/team_store.jsx'); var TeamStore = require('../stores/team_store.jsx');
var BrowserStore = require('../stores/browser_store.jsx'); var BrowserStore = require('../stores/browser_store.jsx');
var utils = require('../utils/utils.jsx'); var Utils = require('../utils/utils.jsx');
var SidebarHeader = require('./sidebar_header.jsx'); var SidebarHeader = require('./sidebar_header.jsx');
var SearchBox = require('./search_bar.jsx'); var SearchBox = require('./search_bar.jsx');
var Constants = require('../utils/constants.jsx'); var Constants = require('../utils/constants.jsx');
function getStateFromStores() { export default class Sidebar extends React.Component {
var members = ChannelStore.getAllMembers(); constructor(props) {
var teamMemberMap = UserStore.getActiveOnlyProfiles(); super(props);
var currentId = ChannelStore.getCurrentId();
var teammates = []; this.badgesActive = false;
for (var id in teamMemberMap) { this.firstUnreadChannel = null;
if (id === UserStore.getCurrentId()) { this.lastUnreadChannel = null;
continue;
} this.onChange = this.onChange.bind(this);
teammates.push(teamMemberMap[id]); this.onScroll = this.onScroll.bind(this);
this.onResize = this.onResize.bind(this);
this.updateUnreadIndicators = this.updateUnreadIndicators.bind(this);
this.createChannelElement = this.createChannelElement.bind(this);
this.state = this.getStateFromStores();
this.state.loadingDMChannel = -1;
} }
getStateFromStores() {
var members = ChannelStore.getAllMembers();
var teamMemberMap = UserStore.getActiveOnlyProfiles();
var currentId = ChannelStore.getCurrentId();
// Create lists of all read and unread direct channels var teammates = [];
var showDirectChannels = []; for (var id in teamMemberMap) {
var readDirectChannels = []; if (id === UserStore.getCurrentId()) {
for (var i = 0; i < teammates.length; i++) { continue;
var teammate = teammates[i]; }
teammates.push(teamMemberMap[id]);
if (teammate.id === UserStore.getCurrentId()) {
continue;
} }
var channelName = ''; // Create lists of all read and unread direct channels
if (teammate.id > UserStore.getCurrentId()) { var showDirectChannels = [];
channelName = UserStore.getCurrentId() + '__' + teammate.id; var readDirectChannels = [];
} else { for (var i = 0; i < teammates.length; i++) {
channelName = teammate.id + '__' + UserStore.getCurrentId(); var teammate = teammates[i];
}
var channel = ChannelStore.getByName(channelName); if (teammate.id === UserStore.getCurrentId()) {
continue;
}
if (channel != null) { var channelName = '';
channel.display_name = teammate.username; if (teammate.id > UserStore.getCurrentId()) {
channel.teammate_username = teammate.username; channelName = UserStore.getCurrentId() + '__' + teammate.id;
channel.status = UserStore.getStatus(teammate.id);
var channelMember = members[channel.id];
var msgCount = channel.total_msg_count - channelMember.msg_count;
if (msgCount > 0) {
showDirectChannels.push(channel);
} else if (currentId === channel.id) {
showDirectChannels.push(channel);
} else { } else {
readDirectChannels.push(channel); channelName = teammate.id + '__' + UserStore.getCurrentId();
}
var channel = ChannelStore.getByName(channelName);
if (channel != null) {
channel.display_name = teammate.username;
channel.teammate_username = teammate.username;
channel.status = UserStore.getStatus(teammate.id);
var channelMember = members[channel.id];
var msgCount = channel.total_msg_count - channelMember.msg_count;
if (msgCount > 0) {
showDirectChannels.push(channel);
} else if (currentId === channel.id) {
showDirectChannels.push(channel);
} else {
readDirectChannels.push(channel);
}
} else {
var tempChannel = {};
tempChannel.fake = true;
tempChannel.name = channelName;
tempChannel.display_name = teammate.username;
tempChannel.teammate_username = teammate.username;
tempChannel.status = UserStore.getStatus(teammate.id);
tempChannel.last_post_at = 0;
tempChannel.total_msg_count = 0;
tempChannel.type = 'D';
readDirectChannels.push(tempChannel);
} }
} else {
var tempChannel = {};
tempChannel.fake = true;
tempChannel.name = channelName;
tempChannel.display_name = teammate.username;
tempChannel.teammate_username = teammate.username;
tempChannel.status = UserStore.getStatus(teammate.id);
tempChannel.last_post_at = 0;
tempChannel.total_msg_count = 0;
tempChannel.type = 'D';
readDirectChannels.push(tempChannel);
} }
}
// If we don't have MAX_DMS unread channels, sort the read list by last_post_at // If we don't have MAX_DMS unread channels, sort the read list by last_post_at
if (showDirectChannels.length < Constants.MAX_DMS) { if (showDirectChannels.length < Constants.MAX_DMS) {
readDirectChannels.sort(function sortByLastPost(a, b) { readDirectChannels.sort(function sortByLastPost(a, b) {
// sort by last_post_at first // sort by last_post_at first
if (a.last_post_at > b.last_post_at) { if (a.last_post_at > b.last_post_at) {
return -1; return -1;
} }
if (a.last_post_at < b.last_post_at) { if (a.last_post_at < b.last_post_at) {
return 1; return 1;
} }
// if last_post_at is equal, sort by name // if last_post_at is equal, sort by name
if (a.display_name < b.display_name) { if (a.display_name < b.display_name) {
return -1; return -1;
} }
if (a.display_name > b.display_name) { if (a.display_name > b.display_name) {
return 1; return 1;
} }
return 0; return 0;
}); });
var index = 0; var index = 0;
while (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) { while (showDirectChannels.length < Constants.MAX_DMS && index < readDirectChannels.length) {
showDirectChannels.push(readDirectChannels[index]); showDirectChannels.push(readDirectChannels[index]);
index++; index++;
}
readDirectChannels = readDirectChannels.slice(index);
showDirectChannels.sort(function directSort(a, b) {
if (a.display_name < b.display_name) {
return -1;
}
if (a.display_name > b.display_name) {
return 1;
}
return 0;
});
} }
readDirectChannels = readDirectChannels.slice(index);
showDirectChannels.sort(function directSort(a, b) { return {
if (a.display_name < b.display_name) { activeId: currentId,
return -1; channels: ChannelStore.getAll(),
} members: members,
if (a.display_name > b.display_name) { showDirectChannels: showDirectChannels,
return 1; hideDirectChannels: readDirectChannels
} };
return 0;
});
} }
componentDidMount() {
return {
activeId: currentId,
channels: ChannelStore.getAll(),
members: members,
showDirectChannels: showDirectChannels,
hideDirectChannels: readDirectChannels
};
}
module.exports = React.createClass({
displayName: 'Sidebar',
propTypes: {
teamType: React.PropTypes.string,
teamDisplayName: React.PropTypes.string
},
componentDidMount: function() {
ChannelStore.addChangeListener(this.onChange); ChannelStore.addChangeListener(this.onChange);
UserStore.addChangeListener(this.onChange); UserStore.addChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange); UserStore.addStatusesChangeListener(this.onChange);
@@ -140,12 +150,12 @@ module.exports = React.createClass({
this.updateUnreadIndicators(); this.updateUnreadIndicators();
$(window).on('resize', this.onResize); $(window).on('resize', this.onResize);
}, }
componentDidUpdate: function() { componentDidUpdate() {
this.updateTitle(); this.updateTitle();
this.updateUnreadIndicators(); this.updateUnreadIndicators();
}, }
componentWillUnmount: function() { componentWillUnmount() {
$(window).off('resize', this.onResize); $(window).off('resize', this.onResize);
ChannelStore.removeChangeListener(this.onChange); ChannelStore.removeChangeListener(this.onChange);
@@ -153,14 +163,14 @@ module.exports = React.createClass({
UserStore.removeStatusesChangeListener(this.onChange); UserStore.removeStatusesChangeListener(this.onChange);
TeamStore.removeChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange);
SocketStore.removeChangeListener(this.onSocketChange); SocketStore.removeChangeListener(this.onSocketChange);
}, }
onChange: function() { onChange() {
var newState = getStateFromStores(); var newState = this.getStateFromStores();
if (!utils.areStatesEqual(newState, this.state)) { if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState); this.setState(newState);
} }
}, }
onSocketChange: function(msg) { onSocketChange(msg) {
if (msg.action === 'posted') { if (msg.action === 'posted') {
if (ChannelStore.getCurrentId() === msg.channel_id) { if (ChannelStore.getCurrentId() === msg.channel_id) {
if (window.isActive) { if (window.isActive) {
@@ -208,17 +218,17 @@ module.exports = React.createClass({
if (notifyText.length === 0) { if (notifyText.length === 0) {
if (msgProps.image) { if (msgProps.image) {
utils.notifyMe(title, username + ' uploaded an image', channel); Utils.notifyMe(title, username + ' uploaded an image', channel);
} else if (msgProps.otherFile) { } else if (msgProps.otherFile) {
utils.notifyMe(title, username + ' uploaded a file', channel); Utils.notifyMe(title, username + ' uploaded a file', channel);
} else { } else {
utils.notifyMe(title, username + ' did something new', channel); Utils.notifyMe(title, username + ' did something new', channel);
} }
} else { } else {
utils.notifyMe(title, username + ' wrote: ' + notifyText, channel); Utils.notifyMe(title, username + ' wrote: ' + notifyText, channel);
} }
if (!user.notify_props || user.notify_props.desktop_sound === 'true') { if (!user.notify_props || user.notify_props.desktop_sound === 'true') {
utils.ding(); Utils.ding();
} }
} }
} else if (msg.action === 'viewed') { } else if (msg.action === 'viewed') {
@@ -243,186 +253,196 @@ module.exports = React.createClass({
} }
} }
} }
}, }
updateTitle: function() { updateTitle() {
var channel = ChannelStore.getCurrent(); var channel = ChannelStore.getCurrent();
if (channel) { if (channel) {
if (channel.type === 'D') { if (channel.type === 'D') {
var teammateUsername = utils.getDirectTeammate(channel.id).username; var teammateUsername = Utils.getDirectTeammate(channel.id).username;
document.title = teammateUsername + ' ' + document.title.substring(document.title.lastIndexOf('-')); document.title = teammateUsername + ' ' + document.title.substring(document.title.lastIndexOf('-'));
} else { } else {
document.title = channel.display_name + ' ' + document.title.substring(document.title.lastIndexOf('-')); document.title = channel.display_name + ' ' + document.title.substring(document.title.lastIndexOf('-'));
} }
} }
}, }
onScroll: function() { onScroll() {
this.updateUnreadIndicators(); this.updateUnreadIndicators();
}, }
onResize: function() { onResize() {
this.updateUnreadIndicators(); this.updateUnreadIndicators();
}, }
updateUnreadIndicators: function() { updateUnreadIndicators() {
var container = $(this.refs.container.getDOMNode()); var container = $(React.findDOMNode(this.refs.container));
if (this.firstUnreadChannel) { if (this.firstUnreadChannel) {
var firstUnreadElement = $(this.refs[this.firstUnreadChannel].getDOMNode()); var firstUnreadElement = $(React.findDOMNode(this.refs[this.firstUnreadChannel]));
if (firstUnreadElement.position().top + firstUnreadElement.height() < 0) { if (firstUnreadElement.position().top + firstUnreadElement.height() < 0) {
$(this.refs.topUnreadIndicator.getDOMNode()).css('display', 'initial'); $(React.findDOMNode(this.refs.topUnreadIndicator)).css('display', 'initial');
} else { } else {
$(this.refs.topUnreadIndicator.getDOMNode()).css('display', 'none'); $(React.findDOMNode(this.refs.topUnreadIndicator)).css('display', 'none');
} }
} }
if (this.lastUnreadChannel) { if (this.lastUnreadChannel) {
var lastUnreadElement = $(this.refs[this.lastUnreadChannel].getDOMNode()); var lastUnreadElement = $(React.findDOMNode(this.refs[this.lastUnreadChannel]));
if (lastUnreadElement.position().top > container.height()) { if (lastUnreadElement.position().top > container.height()) {
$(this.refs.bottomUnreadIndicator.getDOMNode()).css('display', 'initial'); $(React.findDOMNode(this.refs.bottomUnreadIndicator)).css('display', 'initial');
} else { } else {
$(this.refs.bottomUnreadIndicator.getDOMNode()).css('display', 'none'); $(React.findDOMNode(this.refs.bottomUnreadIndicator)).css('display', 'none');
} }
} }
}, }
getInitialState: function() { createChannelElement(channel, index) {
var newState = getStateFromStores();
newState.loadingDMChannel = -1;
return newState;
},
render: function() {
var members = this.state.members; var members = this.state.members;
var activeId = this.state.activeId; var activeId = this.state.activeId;
var badgesActive = false; var channelMember = members[channel.id];
var msgCount;
// keep track of the first and last unread channels so we can use them to set the unread indicators var linkClass = '';
var self = this; if (channel.id === activeId) {
this.firstUnreadChannel = null; linkClass = 'active';
this.lastUnreadChannel = null; }
function createChannelElement(channel, index) { var unread = false;
var channelMember = members[channel.id]; if (channelMember) {
var msgCount; msgCount = channel.total_msg_count - channelMember.msg_count;
unread = (msgCount > 0 && channelMember.notify_level !== 'quiet') || channelMember.mention_count > 0;
}
var linkClass = ''; var titleClass = '';
if (channel.id === activeId) { if (unread) {
linkClass = 'active'; titleClass = 'unread-title';
if (!this.firstUnreadChannel) {
this.firstUnreadChannel = channel.name;
} }
this.lastUnreadChannel = channel.name;
}
var unread = false; var badge = null;
if (channelMember) { if (channelMember) {
msgCount = channel.total_msg_count - channelMember.msg_count;
unread = (msgCount > 0 && channelMember.notify_level !== 'quiet') || channelMember.mention_count > 0;
}
var titleClass = '';
if (unread) {
titleClass = 'unread-title';
if (!self.firstUnreadChannel) {
self.firstUnreadChannel = channel.name;
}
self.lastUnreadChannel = channel.name;
}
var badge = null;
if (channelMember) {
if (channel.type === 'D') {
// direct message channels show badges for any number of unread posts
msgCount = channel.total_msg_count - channelMember.msg_count;
if (msgCount > 0) {
badge = <span className='badge pull-right small'>{msgCount}</span>;
badgesActive = true;
}
} else if (channelMember.mention_count > 0) {
// public and private channels only show badges for mentions
badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>;
badgesActive = true;
}
} else if (self.state.loadingDMChannel === index && channel.type === 'D') {
badge = <img className='channel-loading-gif pull-right' src='/static/images/load.gif'/>;
}
// set up status icon for direct message channels
var status = null;
if (channel.type === 'D') { if (channel.type === 'D') {
var statusIcon = ''; // direct message channels show badges for any number of unread posts
if (channel.status === 'online') { msgCount = channel.total_msg_count - channelMember.msg_count;
statusIcon = Constants.ONLINE_ICON_SVG; if (msgCount > 0) {
} else if (channel.status === 'away') { badge = <span className='badge pull-right small'>{msgCount}</span>;
statusIcon = Constants.ONLINE_ICON_SVG; this.badgesActive = true;
} else {
statusIcon = Constants.OFFLINE_ICON_SVG;
} }
status = <span className='status' dangerouslySetInnerHTML={{__html: statusIcon}} />; } else if (channelMember.mention_count > 0) {
// public and private channels only show badges for mentions
badge = <span className='badge pull-right small'>{channelMember.mention_count}</span>;
this.badgesActive = true;
} }
} else if (this.state.loadingDMChannel === index && channel.type === 'D') {
// set up click handler to switch channels (or create a new channel for non-existant ones) badge = (
var handleClick = null; <img
var href = '#'; className='channel-loading-gif pull-right'
var teamURL = TeamStore.getCurrentTeamUrl(); src='/static/images/load.gif'
/>
if (!channel.fake) {
handleClick = function clickHandler(e) {
e.preventDefault();
utils.switchChannel(channel);
};
} else if (channel.fake && teamURL) {
// It's a direct message channel that doesn't exist yet so let's create it now
var otherUserId = utils.getUserIdFromChannelName(channel);
if (self.state.loadingDMChannel === -1) {
handleClick = function clickHandler(e) {
e.preventDefault();
self.setState({loadingDMChannel: index});
Client.createDirectChannel(channel, otherUserId,
function success(data) {
self.setState({loadingDMChannel: -1});
AsyncClient.getChannel(data.id);
utils.switchChannel(data);
},
function error() {
self.setState({loadingDMChannel: -1});
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
}
);
};
}
}
return (
<li key={channel.name} ref={channel.name} className={linkClass}>
<a className={'sidebar-channel ' + titleClass} href={href} onClick={handleClick}>
{status}
{channel.display_name}
{badge}
</a>
</li>
); );
} }
// set up status icon for direct message channels
var status = null;
if (channel.type === 'D') {
var statusIcon = '';
if (channel.status === 'online') {
statusIcon = Constants.ONLINE_ICON_SVG;
} else if (channel.status === 'away') {
statusIcon = Constants.ONLINE_ICON_SVG;
} else {
statusIcon = Constants.OFFLINE_ICON_SVG;
}
status = (
<span
className='status'
dangerouslySetInnerHTML={{__html: statusIcon}}
/>
);
}
// set up click handler to switch channels (or create a new channel for non-existant ones)
var handleClick = null;
var href = '#';
var teamURL = TeamStore.getCurrentTeamUrl();
if (!channel.fake) {
handleClick = function clickHandler(e) {
e.preventDefault();
Utils.switchChannel(channel);
};
} else if (channel.fake && teamURL) {
// It's a direct message channel that doesn't exist yet so let's create it now
var otherUserId = Utils.getUserIdFromChannelName(channel);
if (this.state.loadingDMChannel === -1) {
handleClick = function clickHandler(e) {
e.preventDefault();
this.setState({loadingDMChannel: index});
Client.createDirectChannel(channel, otherUserId,
function success(data) {
this.setState({loadingDMChannel: -1});
AsyncClient.getChannel(data.id);
Utils.switchChannel(data);
},
function error() {
this.setState({loadingDMChannel: -1});
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channel.name;
}
);
};
}
}
return (
<li
key={channel.name}
ref={channel.name}
className={linkClass}
>
<a
className={'sidebar-channel ' + titleClass}
href={href}
onClick={handleClick}
>
{status}
{channel.display_name}
{badge}
</a>
</li>
);
}
render() {
this.badgesActive = false;
// keep track of the first and last unread channels so we can use them to set the unread indicators
this.firstUnreadChannel = null;
this.lastUnreadChannel = null;
// create elements for all 3 types of channels // create elements for all 3 types of channels
var channelItems = this.state.channels.filter( var channelItems = this.state.channels.filter(
function filterPublicChannels(channel) { function filterPublicChannels(channel) {
return channel.type === 'O'; return channel.type === 'O';
} }
).map(createChannelElement); ).map(this.createChannelElement);
var privateChannelItems = this.state.channels.filter( var privateChannelItems = this.state.channels.filter(
function filterPrivateChannels(channel) { function filterPrivateChannels(channel) {
return channel.type === 'P'; return channel.type === 'P';
} }
).map(createChannelElement); ).map(this.createChannelElement);
var directMessageItems = this.state.showDirectChannels.map(createChannelElement); var directMessageItems = this.state.showDirectChannels.map(this.createChannelElement);
// update the favicon to show if there are any notifications // update the favicon to show if there are any notifications
var link = document.createElement('link'); var link = document.createElement('link');
link.type = 'image/x-icon'; link.type = 'image/x-icon';
link.rel = 'shortcut icon'; link.rel = 'shortcut icon';
link.id = 'favicon'; link.id = 'favicon';
if (badgesActive) { if (this.badgesActive) {
link.href = '/static/images/redfavicon.ico'; link.href = '/static/images/redfavicon.ico';
} else { } else {
link.href = '/static/images/favicon.ico'; link.href = '/static/images/favicon.ico';
@@ -438,7 +458,13 @@ module.exports = React.createClass({
if (this.state.hideDirectChannels.length > 0) { if (this.state.hideDirectChannels.length > 0) {
directMessageMore = ( directMessageMore = (
<li> <li>
<a href='#' data-toggle='modal' className='nav-more' data-target='#more_direct_channels' data-channels={JSON.stringify(this.state.hideDirectChannels)}> <a
href='#'
data-toggle='modal'
className='nav-more'
data-target='#more_direct_channels'
data-channels={JSON.stringify(this.state.hideDirectChannels)}
>
{'More (' + this.state.hideDirectChannels.length + ')'} {'More (' + this.state.hideDirectChannels.length + ')'}
</a> </a>
</li> </li>
@@ -447,21 +473,76 @@ module.exports = React.createClass({
return ( return (
<div> <div>
<SidebarHeader teamDisplayName={this.props.teamDisplayName} teamType={this.props.teamType} /> <SidebarHeader
teamDisplayName={this.props.teamDisplayName}
teamType={this.props.teamType}
/>
<SearchBox /> <SearchBox />
<div ref='topUnreadIndicator' className='nav-pills__unread-indicator nav-pills__unread-indicator-top' style={{display: 'none'}}>Unread post(s) above</div> <div
<div ref='bottomUnreadIndicator' className='nav-pills__unread-indicator nav-pills__unread-indicator-bottom' style={{display: 'none'}}>Unread post(s) below</div> ref='topUnreadIndicator'
className='nav-pills__unread-indicator nav-pills__unread-indicator-top'
style={{display: 'none'}}
>
Unread post(s) above
</div>
<div
ref='bottomUnreadIndicator'
className='nav-pills__unread-indicator nav-pills__unread-indicator-bottom'
style={{display: 'none'}}
>
Unread post(s) below
</div>
<div ref='container' className='nav-pills__container' onScroll={this.onScroll}> <div
ref='container'
className='nav-pills__container'
onScroll={this.onScroll}
>
<ul className='nav nav-pills nav-stacked'> <ul className='nav nav-pills nav-stacked'>
<li><h4>Channels<a className='add-channel-btn' href='#' data-toggle='modal' data-target='#new_channel' data-channeltype='O'>+</a></h4></li> <li>
<h4>
Channels
<a
className='add-channel-btn'
href='#'
data-toggle='modal'
data-target='#new_channel'
data-channeltype='O'
>
+
</a>
</h4>
</li>
{channelItems} {channelItems}
<li><a href='#' data-toggle='modal' className='nav-more' data-target='#more_channels' data-channeltype='O'>More...</a></li> <li>
<a
href='#'
data-toggle='modal'
className='nav-more'
data-target='#more_channels'
data-channeltype='O'
>
More...
</a>
</li>
</ul> </ul>
<ul className='nav nav-pills nav-stacked'> <ul className='nav nav-pills nav-stacked'>
<li><h4>Private Groups<a className='add-channel-btn' href='#' data-toggle='modal' data-target='#new_channel' data-channeltype='P'>+</a></h4></li> <li>
<h4>
Private Groups
<a
className='add-channel-btn'
href='#'
data-toggle='modal'
data-target='#new_channel'
data-channeltype='P'
>
+
</a>
</h4>
</li>
{privateChannelItems} {privateChannelItems}
</ul> </ul>
<ul className='nav nav-pills nav-stacked'> <ul className='nav nav-pills nav-stacked'>
@@ -473,4 +554,13 @@ module.exports = React.createClass({
</div> </div>
); );
} }
}); }
Sidebar.defaultProps = {
teamType: '',
teamDisplayName: ''
};
Sidebar.propTypes = {
teamType: React.PropTypes.string,
teamDisplayName: React.PropTypes.string
};

Просмотреть файл

@@ -4,37 +4,27 @@
var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMin = require('./setting_item_min.jsx');
var SettingItemMax = require('./setting_item_max.jsx'); var SettingItemMax = require('./setting_item_max.jsx');
var client = require('../utils/client.jsx'); var Client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx'); var AsyncClient = require('../utils/async_client.jsx');
module.exports = React.createClass({ export default class FeatureTab extends React.Component {
displayName: 'Feature Tab', constructor(props) {
propTypes: { super(props);
updateSection: React.PropTypes.func.isRequired,
team: React.PropTypes.object.isRequired,
activeSection: React.PropTypes.string.isRequired
},
submitValetFeature: function() {
var data = {};
data.allow_valet = this.state.allowValet;
client.updateValetFeature(data, this.submitValetFeature = this.submitValetFeature.bind(this);
function() { this.handleValetRadio = this.handleValetRadio.bind(this);
this.props.updateSection(''); this.onUpdateSection = this.onUpdateSection.bind(this);
AsyncClient.getMyTeam();
}.bind(this), this.state = {};
function(err) { var team = this.props.team;
var state = this.getInitialState();
state.serverError = err; if (team && team.allow_valet) {
this.setState(state); this.state.allowValet = 'true';
}.bind(this) } else {
); this.state.allowValet = 'false';
}, }
handleValetRadio: function(val) { }
this.setState({allowValet: val}); componentWillReceiveProps(newProps) {
this.refs.wrapper.getDOMNode().focus();
},
componentWillReceiveProps: function(newProps) {
var team = newProps.team; var team = newProps.team;
var allowValet = 'false'; var allowValet = 'false';
@@ -43,26 +33,36 @@ module.exports = React.createClass({
} }
this.setState({allowValet: allowValet}); this.setState({allowValet: allowValet});
}, }
getInitialState: function() { submitValetFeature() {
var team = this.props.team; var data = {};
data.allow_valet = this.state.allowValet;
var allowValet = 'false'; Client.updateValetFeature(data,
if (team && team.allow_valet) { function success() {
allowValet = 'true'; this.props.updateSection('');
} AsyncClient.getMyTeam();
}.bind(this),
return {allowValet: allowValet}; function fail(err) {
}, var state = this.getInitialState();
onUpdateSection: function(e) { state.serverError = err;
this.setState(state);
}.bind(this)
);
}
handleValetRadio(val) {
this.setState({allowValet: val});
React.findDOMNode(this.refs.wrapper).focus();
}
onUpdateSection(e) {
e.preventDefault(); e.preventDefault();
if (this.props.activeSection === 'valet') { if (this.props.activeSection === 'valet') {
this.props.updateSection(''); this.props.updateSection('');
} else { } else {
this.props.updateSection('valet'); this.props.updateSection('valet');
} }
}, }
render: function() { render() {
var clientError = null; var clientError = null;
var serverError = null; var serverError = null;
if (this.state.clientError) { if (this.state.clientError) {
@@ -73,7 +73,6 @@ module.exports = React.createClass({
} }
var valetSection; var valetSection;
var self = this;
if (this.props.activeSection === 'valet') { if (this.props.activeSection === 'valet') {
var valetActive = [false, false]; var valetActive = [false, false];
@@ -92,7 +91,7 @@ module.exports = React.createClass({
<input <input
type='radio' type='radio'
checked={valetActive[0]} checked={valetActive[0]}
onChange={self.handleValetRadio.bind(this, 'true')} onChange={this.handleValetRadio.bind(this, 'true')}
> >
On On
</input> </input>
@@ -104,7 +103,7 @@ module.exports = React.createClass({
<input <input
type='radio' type='radio'
checked={valetActive[1]} checked={valetActive[1]}
onChange={self.handleValetRadio.bind(this, 'false')} onChange={this.handleValetRadio.bind(this, 'false')}
> >
Off Off
</input> </input>
@@ -145,10 +144,25 @@ module.exports = React.createClass({
return ( return (
<div> <div>
<div className='modal-header'> <div className='modal-header'>
<button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>&times;</span></button> <button
<h4 className='modal-title' ref='title'><i className='modal-back'></i>Advanced Features</h4> type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>&times;</span>
</button>
<h4
className='modal-title'
ref='title'
>
<i className='modal-back'></i>Advanced Features
</h4>
</div> </div>
<div ref='wrapper' className='user-settings'> <div
ref='wrapper'
className='user-settings'
>
<h3 className='tab-header'>Advanced Features</h3> <h3 className='tab-header'>Advanced Features</h3>
<div className='divider-dark first'/> <div className='divider-dark first'/>
{valetSection} {valetSection}
@@ -157,4 +171,14 @@ module.exports = React.createClass({
</div> </div>
); );
} }
}); }
FeatureTab.defaultProps = {
team: {},
activeSection: ''
};
FeatureTab.propTypes = {
updateSection: React.PropTypes.func.isRequired,
team: React.PropTypes.object.isRequired,
activeSection: React.PropTypes.string.isRequired
};

Просмотреть файл

@@ -5,66 +5,83 @@ var TeamStore = require('../stores/team_store.jsx');
var ImportTab = require('./team_import_tab.jsx'); var ImportTab = require('./team_import_tab.jsx');
var FeatureTab = require('./team_feature_tab.jsx'); var FeatureTab = require('./team_feature_tab.jsx');
var GeneralTab = require('./team_general_tab.jsx'); var GeneralTab = require('./team_general_tab.jsx');
var utils = require('../utils/utils.jsx'); var Utils = require('../utils/utils.jsx');
module.exports = React.createClass({ export default class TeamSettings extends React.Component {
displayName: 'Team Settings', constructor(props) {
propTypes: { super(props);
activeTab: React.PropTypes.string.isRequired,
activeSection: React.PropTypes.string.isRequired, this.onChange = this.onChange.bind(this);
updateSection: React.PropTypes.func.isRequired,
teamDisplayName: React.PropTypes.string.isRequired this.state = {team: TeamStore.getCurrent()};
}, }
componentDidMount: function() { componentDidMount() {
TeamStore.addChangeListener(this.onChange); TeamStore.addChangeListener(this.onChange);
}, }
componentWillUnmount: function() { componentWillUnmount() {
TeamStore.removeChangeListener(this.onChange); TeamStore.removeChangeListener(this.onChange);
}, }
onChange: function() { onChange() {
var team = TeamStore.getCurrent(); var team = TeamStore.getCurrent();
if (!utils.areStatesEqual(this.state.team, team)) { if (!Utils.areStatesEqual(this.state.team, team)) {
this.setState({team: team}); this.setState({team: team});
} }
}, }
getInitialState: function() { render() {
return {team: TeamStore.getCurrent()};
},
render: function() {
var result; var result;
switch (this.props.activeTab) { switch (this.props.activeTab) {
case 'general': case 'general':
result = ( result = (
<div> <div>
<GeneralTab <GeneralTab
team={this.state.team} team={this.state.team}
activeSection={this.props.activeSection} activeSection={this.props.activeSection}
updateSection={this.props.updateSection} updateSection={this.props.updateSection}
teamDisplayName={this.props.teamDisplayName} teamDisplayName={this.props.teamDisplayName}
/> />
</div> </div>
); );
break; break;
case 'feature': case 'feature':
result = ( result = (
<div> <div>
<FeatureTab team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> <FeatureTab
</div> team={this.state.team}
); activeSection={this.props.activeSection}
break; updateSection={this.props.updateSection}
case 'import': />
result = ( </div>
<div> );
<ImportTab team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} /> break;
</div> case 'import':
); result = (
break; <div>
default: <ImportTab
result = ( team={this.state.team}
<div/> activeSection={this.props.activeSection}
); updateSection={this.props.updateSection}
break; />
</div>
);
break;
default:
result = (
<div/>
);
break;
} }
return result; return result;
} }
}); }
TeamSettings.defaultProps = {
activeTab: '',
activeSection: '',
teamDisplayName: ''
};
TeamSettings.propTypes = {
activeTab: React.PropTypes.string.isRequired,
activeSection: React.PropTypes.string.isRequired,
updateSection: React.PropTypes.func.isRequired,
teamDisplayName: React.PropTypes.string.isRequired
};

Просмотреть файл

@@ -1,31 +1,34 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
var client = require('../utils/client.jsx'); var Client = require('../utils/client.jsx');
module.exports = React.createClass({ export default class TeamSignupAllowedDomainsPage extends React.Component {
displayName: 'TeamSignupAllowedDomainsPage', constructor(props) {
propTypes: { super(props);
state: React.PropTypes.object,
updateParent: React.PropTypes.func this.submitBack = this.submitBack.bind(this);
}, this.submitNext = this.submitNext.bind(this);
submitBack: function(e) {
this.state = {};
}
submitBack(e) {
e.preventDefault(); e.preventDefault();
this.props.state.wizard = 'team_url'; this.props.state.wizard = 'team_url';
this.props.updateParent(this.props.state); this.props.updateParent(this.props.state);
}, }
submitNext: function(e) { submitNext(e) {
e.preventDefault(); e.preventDefault();
if (this.refs.open_network.getDOMNode().checked) { if (React.findDOMNode(this.refs.open_network).checked) {
this.props.state.wizard = 'send_invites'; this.props.state.wizard = 'send_invites';
this.props.state.team.type = 'O'; this.props.state.team.type = 'O';
this.props.updateParent(this.props.state); this.props.updateParent(this.props.state);
return; return;
} }
if (this.refs.allow.getDOMNode().checked) { if (React.findDOMNode(this.refs.allow).checked) {
var name = this.refs.name.getDOMNode().value.trim(); var name = React.findDOMNode(this.refs.name).value.trim();
var domainRegex = /^\w+\.\w+$/; var domainRegex = /^\w+\.\w+$/;
if (!name) { if (!name) {
this.setState({nameError: 'This field is required'}); this.setState({nameError: 'This field is required'});
@@ -46,12 +49,9 @@ module.exports = React.createClass({
this.props.state.team.type = 'I'; this.props.state.team.type = 'I';
this.props.updateParent(this.props.state); this.props.updateParent(this.props.state);
} }
}, }
getInitialState: function() { render() {
return {}; Client.track('signup', 'signup_team_04_allow_domains');
},
render: function() {
client.track('signup', 'signup_team_04_allow_domains');
var nameError = null; var nameError = null;
var nameDivClass = 'form-group'; var nameDivClass = 'form-group';
@@ -63,11 +63,21 @@ module.exports = React.createClass({
return ( return (
<div> <div>
<form> <form>
<img className='signup-team-logo' src='/static/images/logo.png' /> <img
className='signup-team-logo'
src='/static/images/logo.png'
/>
<h2>Email Domain</h2> <h2>Email Domain</h2>
<p> <p>
<div className='checkbox'> <div className='checkbox'>
<label><input type='checkbox' ref='allow' defaultChecked={true} />{' Allow sign up and ' + strings.Team + ' discovery with a ' + strings.Company + ' email address.'}</label> <label>
<input
type='checkbox'
ref='allow'
defaultChecked={true}
/>
{' Allow sign up and ' + strings.Team + ' discovery with a ' + strings.Company + ' email address.'}
</label>
</div> </div>
</p> </p>
<p>{'Check this box to allow your ' + strings.Team + ' members to sign up using their ' + strings.Company + ' email addresses if you share the same domain--otherwise, you need to invite everyone yourself.'}</p> <p>{'Check this box to allow your ' + strings.Team + ' members to sign up using their ' + strings.Company + ' email addresses if you share the same domain--otherwise, you need to invite everyone yourself.'}</p>
@@ -77,7 +87,16 @@ module.exports = React.createClass({
<div className='col-sm-9'> <div className='col-sm-9'>
<div className='input-group'> <div className='input-group'>
<span className='input-group-addon'>@</span> <span className='input-group-addon'>@</span>
<input type='text' ref='name' className='form-control' placeholder='' maxLength='128' defaultValue={this.props.state.team.allowed_domains} autoFocus={true} onFocus={this.handleFocus}/> <input
type='text'
ref='name'
className='form-control'
placeholder=''
maxLength='128'
defaultValue={this.props.state.team.allowed_domains}
autoFocus={true}
onFocus={this.handleFocus}
/>
</div> </div>
</div> </div>
</div> </div>
@@ -86,13 +105,38 @@ module.exports = React.createClass({
<p>To allow signups from multiple domains, separate each with a comma.</p> <p>To allow signups from multiple domains, separate each with a comma.</p>
<p> <p>
<div className='checkbox'> <div className='checkbox'>
<label><input type='checkbox' ref='open_network' defaultChecked={this.props.state.team.type === 'O'} /> Allow anyone to signup to this domain without an invitation.</label> <label>
<input
type='checkbox'
ref='open_network'
defaultChecked={this.props.state.team.type === 'O'}
/> Allow anyone to signup to this domain without an invitation.</label>
</div> </div>
</p> </p>
<button type='button' className='btn btn-default' onClick={this.submitBack}><i className='glyphicon glyphicon-chevron-left'></i> Back</button>&nbsp; <button
<button type='submit' className='btn-primary btn' onClick={this.submitNext}>Next<i className='glyphicon glyphicon-chevron-right'></i></button> type='button'
className='btn btn-default'
onClick={this.submitBack}
>
<i className='glyphicon glyphicon-chevron-left'></i> Back
</button>&nbsp;
<button
type='submit'
className='btn-primary btn'
onClick={this.submitNext}
>
Next<i className='glyphicon glyphicon-chevron-right'></i>
</button>
</form> </form>
</div> </div>
); );
} }
}); }
TeamSignupAllowedDomainsPage.defaultProps = {
state: {}
};
TeamSignupAllowedDomainsPage.propTypes = {
state: React.PropTypes.object,
updateParent: React.PropTypes.func
};

Просмотреть файл

@@ -1,24 +1,28 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
var utils = require('../utils/utils.jsx'); var Client = require('../utils/client.jsx');
var client = require('../utils/client.jsx'); var BrowserStore = require('../stores/browser_store.jsx');
var UserStore = require('../stores/user_store.jsx');
module.exports = React.createClass({ export default class TeamSignupPasswordPage extends React.Component {
displayName: 'TeamSignupPasswordPage', constructor(props) {
propTypes: { super(props);
state: React.PropTypes.object,
updateParent: React.PropTypes.func this.submitBack = this.submitBack.bind(this);
}, this.submitNext = this.submitNext.bind(this);
submitBack: function(e) {
this.state = {};
}
submitBack(e) {
e.preventDefault(); e.preventDefault();
this.props.state.wizard = 'username'; this.props.state.wizard = 'username';
this.props.updateParent(this.props.state); this.props.updateParent(this.props.state);
}, }
submitNext: function(e) { submitNext(e) {
e.preventDefault(); e.preventDefault();
var password = this.refs.password.getDOMNode().value.trim(); var password = React.findDOMNode(this.refs.password).value.trim();
if (!password || password.length < 5) { if (!password || password.length < 5) {
this.setState({passwordError: 'Please enter at least 5 characters'}); this.setState({passwordError: 'Please enter at least 5 characters'});
return; return;
@@ -31,15 +35,14 @@ module.exports = React.createClass({
teamSignup.user.allow_marketing = true; teamSignup.user.allow_marketing = true;
delete teamSignup.wizard; delete teamSignup.wizard;
client.createTeamFromSignup(teamSignup, Client.createTeamFromSignup(teamSignup,
function success() { function success() {
client.track('signup', 'signup_team_08_complete'); Client.track('signup', 'signup_team_08_complete');
var props = this.props; var props = this.props;
Client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password,
client.loginByEmail(teamSignup.team.name, teamSignup.team.email, teamSignup.user.password, function loginSuccess() {
function(data) {
UserStore.setLastEmail(teamSignup.team.email); UserStore.setLastEmail(teamSignup.team.email);
UserStore.setCurrentUser(data); UserStore.setCurrentUser(data);
if (this.props.hash > 0) { if (this.props.hash > 0) {
@@ -52,7 +55,7 @@ module.exports = React.createClass({
window.location.href = '/'; window.location.href = '/';
}.bind(this), }.bind(this),
function(err) { function loginFail(err) {
if (err.message === 'Login failed because email address has not been verified') { if (err.message === 'Login failed because email address has not been verified') {
window.location.href = '/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name); window.location.href = '/verify_email?email=' + encodeURIComponent(teamSignup.team.email) + '&teamname=' + encodeURIComponent(teamSignup.team.name);
} else { } else {
@@ -67,12 +70,9 @@ module.exports = React.createClass({
$('#finish-button').button('reset'); $('#finish-button').button('reset');
}.bind(this) }.bind(this)
); );
}, }
getInitialState: function() { render() {
return {}; Client.track('signup', 'signup_team_07_password');
},
render: function() {
client.track('signup', 'signup_team_07_password');
var passwordError = null; var passwordError = null;
var passwordDivStyle = 'form-group'; var passwordDivStyle = 'form-group';
@@ -89,7 +89,10 @@ module.exports = React.createClass({
return ( return (
<div> <div>
<form> <form>
<img className='signup-team-logo' src='/static/images/logo.png' /> <img
className='signup-team-logo'
src='/static/images/logo.png'
/>
<h2 className='margin--less'>Your password</h2> <h2 className='margin--less'>Your password</h2>
<h5 className='color--light'>Select a password that you'll use to login with your email address:</h5> <h5 className='color--light'>Select a password that you'll use to login with your email address:</h5>
<div className='inner__content margin--extra'> <div className='inner__content margin--extra'>
@@ -99,7 +102,14 @@ module.exports = React.createClass({
<div className='row'> <div className='row'>
<div className='col-sm-11'> <div className='col-sm-11'>
<h5><strong>Choose your password</strong></h5> <h5><strong>Choose your password</strong></h5>
<input autoFocus={true} type='password' ref='password' className='form-control' placeholder='' maxLength='128' /> <input
autoFocus={true}
type='password'
ref='password'
className='form-control'
placeholder=''
maxLength='128'
/>
<div className='color--light form__hint'>Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</div> <div className='color--light form__hint'>Passwords must contain 5 to 50 characters. Your password will be strongest if it contains a mix of symbols, numbers, and upper and lowercase characters.</div>
</div> </div>
</div> </div>
@@ -108,14 +118,37 @@ module.exports = React.createClass({
</div> </div>
</div> </div>
<div className='form-group'> <div className='form-group'>
<button type='submit' className='btn btn-primary margin--extra' id='finish-button' data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating ' + strings.Team + '...'} onClick={this.submitNext}>Finish</button> <button
type='submit'
className='btn btn-primary margin--extra'
id='finish-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Creating ' + strings.Team + '...'}
onClick={this.submitNext}
>
Finish
</button>
</div> </div>
<p>By proceeding to create your account and use {config.SiteName}, you agree to our <a href={config.TermsLink}>Terms of Service</a> and <a href={config.PrivacyLink}>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p> <p>By proceeding to create your account and use {config.SiteName}, you agree to our <a href={config.TermsLink}>Terms of Service</a> and <a href={config.PrivacyLink}>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p>
<div className='margin--extra'> <div className='margin--extra'>
<a href='#' onClick={this.submitBack}>Back to previous step</a> <a
href='#'
onClick={this.submitBack}
>
Back to previous step
</a>
</div> </div>
</form> </form>
</div> </div>
); );
} }
}); }
TeamSignupPasswordPage.defaultProps = {
state: {},
hash: ''
};
TeamSignupPasswordPage.propTypes = {
state: React.PropTypes.object,
hash: React.PropTypes.string,
updateParent: React.PropTypes.func
};

Просмотреть файл

@@ -1,20 +1,9 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
var Utils = require('../utils/utils.jsx');
var utils = require('../utils/utils.jsx');
var UserStore = require('../stores/user_store.jsx'); var UserStore = require('../stores/user_store.jsx');
function getStateFromStores(userId) {
var profile = UserStore.getProfile(userId);
if (profile == null) {
return { profile: { id: "0", username: "..."} };
} else {
return { profile: profile };
}
}
var id = 0; var id = 0;
function nextId() { function nextId() {
@@ -22,49 +11,76 @@ function nextId() {
return id; return id;
} }
export default class UserProfile extends React.Component {
constructor(props) {
super(props);
module.exports = React.createClass({ this.uniqueId = nextId();
uniqueId: null,
componentDidMount: function() { this.state = this.getStateFromStores(this.props.userId);
UserStore.addChangeListener(this._onChange); }
$("#profile_" + this.uniqueId).popover({placement : 'right', container: 'body', trigger: 'hover', html: true, delay: { "show": 200, "hide": 100 }}); getStateFromStores(userId) {
$('body').tooltip( {selector: '[data-toggle=tooltip]', trigger: 'hover click'} ); var profile = UserStore.getProfile(userId);
},
componentWillUnmount: function() { if (profile == null) {
UserStore.removeChangeListener(this._onChange); return {profile: {id: '0', username: '...'}};
}, }
_onChange: function(id) {
if (id == this.props.userId) { return {profile: profile};
var newState = getStateFromStores(this.props.userId); }
if (!utils.areStatesEqual(newState, this.state)) { componentDidMount() {
UserStore.addChangeListener(this.onChange);
$('#profile_' + this.uniqueId).popover({placement: 'right', container: 'body', trigger: 'hover', html: true, delay: {show: 200, hide: 100}});
$('body').tooltip({selector: '[data-toggle=tooltip]', trigger: 'hover click'});
}
componentWillUnmount() {
UserStore.removeChangeListener(this.onChange);
}
onChange(userId) {
if (userId === this.props.userId) {
var newState = this.getStateFromStores(this.props.userId);
if (!Utils.areStatesEqual(newState, this.state)) {
this.setState(newState); this.setState(newState);
} }
} }
}, }
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps(nextProps) {
if (this.props.userId != nextProps.userId) { if (this.props.userId !== nextProps.userId) {
this.setState(getStateFromStores(nextProps.userId)); this.setState(this.getStateFromStores(nextProps.userId));
}
}
render() {
var name = this.state.profile.username;
if (this.props.overwriteName) {
name = this.props.overwriteName;
} }
},
getInitialState: function() {
this.uniqueId = nextId();
return getStateFromStores(this.props.userId);
},
render: function() {
var name = this.props.overwriteName ? this.props.overwriteName : this.state.profile.username;
var dataContent = '<img class="user-popover__image" src="/api/v1/users/' + this.state.profile.id + '/image?time=' + this.state.profile.update_at + '" height="128" width="128" />';
var data_content = "<img class='user-popover__image' src='/api/v1/users/" + this.state.profile.id + "/image?time=" + this.state.profile.update_at + "' height='128' width='128' />";
if (!config.ShowEmail) { if (!config.ShowEmail) {
data_content += "<div class='text-nowrap'>Email not shared</div>"; dataContent += '<div class="text-nowrap">Email not shared</div>';
} else { } else {
data_content += "<div data-toggle='tooltip' title= '" + this.state.profile.email + "'><a href='mailto:" + this.state.profile.email + "' class='text-nowrap text-lowercase user-popover__email'>" + this.state.profile.email + "</a></div>"; dataContent += '<div data-toggle="tooltip" title="' + this.state.profile.email + '"><a href="mailto:' + this.state.profile.email + '" class="text-nowrap text-lowercase user-popover__email">' + this.state.profile.email + '</a></div>';
} }
return ( return (
<div className="user-popover" id={"profile_" + this.uniqueId} data-toggle="popover" data-content={data_content} data-original-title={this.state.profile.username} > <div
{ name } className='user-popover'
id={'profile_' + this.uniqueId}
data-toggle='popover'
data-content={dataContent}
data-original-title={this.state.profile.username}
>
{name}
</div> </div>
); );
} }
}); }
UserProfile.defaultProps = {
userId: '',
overwriteName: ''
};
UserProfile.propTypes = {
userId: React.PropTypes.string,
overwriteName: React.PropTypes.string
};

Просмотреть файл

@@ -4,100 +4,133 @@
var UserStore = require('../stores/user_store.jsx'); var UserStore = require('../stores/user_store.jsx');
var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMin = require('./setting_item_min.jsx');
var SettingItemMax = require('./setting_item_max.jsx'); var SettingItemMax = require('./setting_item_max.jsx');
var client = require('../utils/client.jsx'); var Client = require('../utils/client.jsx');
var utils = require('../utils/utils.jsx'); var Utils = require('../utils/utils.jsx');
module.exports = React.createClass({ export default class UserSettingsAppearance extends React.Component {
submitTheme: function(e) { constructor(props) {
e.preventDefault(); super(props);
var user = UserStore.getCurrentUser();
if (!user.props) user.props = {};
user.props.theme = this.state.theme;
client.updateUser(user, this.submitTheme = this.submitTheme.bind(this);
function(data) { this.updateTheme = this.updateTheme.bind(this);
this.props.updateSection(""); this.handleClose = this.handleClose.bind(this);
window.location.reload();
}.bind(this), this.state = this.getStateFromStores();
function(err) { }
state = this.getInitialState(); getStateFromStores() {
state.server_error = err;
this.setState(state);
}.bind(this)
);
},
updateTheme: function(e) {
var hex = utils.rgb2hex(e.target.style.backgroundColor);
this.setState({ theme: hex.toLowerCase() });
},
handleClose: function() {
this.setState({server_error: null});
this.props.updateTab('general');
},
componentDidMount: function() {
if (this.props.activeSection === "theme") {
$(this.refs[this.state.theme].getDOMNode()).addClass('active-border');
}
$('#user_settings').on('hidden.bs.modal', this.handleClose);
},
componentDidUpdate: function() {
if (this.props.activeSection === "theme") {
$('.color-btn').removeClass('active-border');
$(this.refs[this.state.theme].getDOMNode()).addClass('active-border');
}
},
componentWillUnmount: function() {
$('#user_settings').off('hidden.bs.modal', this.handleClose);
this.props.updateSection('');
},
getInitialState: function() {
var user = UserStore.getCurrentUser(); var user = UserStore.getCurrentUser();
var theme = config.ThemeColors != null ? config.ThemeColors[0] : "#2389d7"; var theme = '#2389d7';
if (config.ThemeColors != null) {
theme = config.ThemeColors[0];
}
if (user.props && user.props.theme) { if (user.props && user.props.theme) {
theme = user.props.theme; theme = user.props.theme;
} }
return { theme: theme.toLowerCase() };
},
render: function() {
var server_error = this.state.server_error ? this.state.server_error : null;
return {theme: theme.toLowerCase()};
}
submitTheme(e) {
e.preventDefault();
var user = UserStore.getCurrentUser();
if (!user.props) {
user.props = {};
}
user.props.theme = this.state.theme;
Client.updateUser(user,
function success() {
this.props.updateSection('');
window.location.reload();
}.bind(this),
function fail(err) {
var state = this.getStateFromStores();
state.serverError = err;
this.setState(state);
}.bind(this)
);
}
updateTheme(e) {
var hex = Utils.rgb2hex(e.target.style.backgroundColor);
this.setState({theme: hex.toLowerCase()});
}
handleClose() {
this.setState({serverError: null});
this.props.updateTab('general');
}
componentDidMount() {
if (this.props.activeSection === 'theme') {
$(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
}
$('#user_settings').on('hidden.bs.modal', this.handleClose);
}
componentDidUpdate() {
if (this.props.activeSection === 'theme') {
$('.color-btn').removeClass('active-border');
$(React.findDOMNode(this.refs[this.state.theme])).addClass('active-border');
}
}
componentWillUnmount() {
$('#user_settings').off('hidden.bs.modal', this.handleClose);
this.props.updateSection('');
}
render() {
var serverError;
if (this.state.serverError) {
serverError = this.state.serverError;
}
var themeSection; var themeSection;
var self = this; var self = this;
if (config.ThemeColors != null) { if (config.ThemeColors != null) {
if (this.props.activeSection === 'theme') { if (this.props.activeSection === 'theme') {
var theme_buttons = []; var themeButtons = [];
for (var i = 0; i < config.ThemeColors.length; i++) { for (var i = 0; i < config.ThemeColors.length; i++) {
theme_buttons.push(<button ref={config.ThemeColors[i]} type="button" className="btn btn-lg color-btn" style={{backgroundColor: config.ThemeColors[i]}} onClick={this.updateTheme} />); themeButtons.push(
<button
ref={config.ThemeColors[i]}
type='button'
className='btn btn-lg color-btn'
style={{backgroundColor: config.ThemeColors[i]}}
onClick={this.updateTheme}
/>
);
} }
var inputs = []; var inputs = [];
inputs.push( inputs.push(
<li className="setting-list-item"> <li className='setting-list-item'>
<div className="btn-group" data-toggle="buttons-radio"> <div
{ theme_buttons } className='btn-group'
data-toggle='buttons-radio'
>
{themeButtons}
</div> </div>
</li> </li>
); );
themeSection = ( themeSection = (
<SettingItemMax <SettingItemMax
title="Theme Color" title='Theme Color'
inputs={inputs} inputs={inputs}
submit={this.submitTheme} submit={this.submitTheme}
server_error={server_error} serverError={serverError}
updateSection={function(e){self.props.updateSection("");e.preventDefault;}} updateSection={function updateSection(e) {
self.props.updateSection('');
e.preventDefault();
}}
/> />
); );
} else { } else {
themeSection = ( themeSection = (
<SettingItemMin <SettingItemMin
title="Theme Color" title='Theme Color'
describe={this.state.theme} describe={this.state.theme}
updateSection={function(){self.props.updateSection("theme");}} updateSection={function updateSection() {
self.props.updateSection('theme');
}}
/> />
); );
} }
@@ -105,17 +138,38 @@ module.exports = React.createClass({
return ( return (
<div> <div>
<div className="modal-header"> <div className='modal-header'>
<button type="button" className="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button> <button
<h4 className="modal-title" ref="title"><i className="modal-back"></i>Appearance Settings</h4> type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>&times;</span>
</button>
<h4
className='modal-title'
ref='title'
>
<i className='modal-back'></i>Appearance Settings
</h4>
</div> </div>
<div className="user-settings"> <div className='user-settings'>
<h3 className="tab-header">Appearance Settings</h3> <h3 className='tab-header'>Appearance Settings</h3>
<div className="divider-dark first"/> <div className='divider-dark first'/>
{themeSection} {themeSection}
<div className="divider-dark"/> <div className='divider-dark'/>
</div> </div>
</div> </div>
); );
} }
}); }
UserSettingsAppearance.defaultProps = {
activeSection: ''
};
UserSettingsAppearance.propTypes = {
activeSection: React.PropTypes.string,
updateSection: React.PropTypes.func,
updateTab: React.PropTypes.func
};

Просмотреть файл

@@ -3,13 +3,23 @@
var SettingItemMin = require('./setting_item_min.jsx'); var SettingItemMin = require('./setting_item_min.jsx');
var SettingItemMax = require('./setting_item_max.jsx'); var SettingItemMax = require('./setting_item_max.jsx');
var client = require('../utils/client.jsx'); var Client = require('../utils/client.jsx');
var AsyncClient = require('../utils/async_client.jsx'); var AsyncClient = require('../utils/async_client.jsx');
var Constants = require('../utils/constants.jsx'); var Constants = require('../utils/constants.jsx');
module.exports = React.createClass({ export default class SecurityTab extends React.Component {
displayName: 'SecurityTab', constructor(props) {
submitPassword: function(e) { super(props);
this.submitPassword = this.submitPassword.bind(this);
this.updateCurrentPassword = this.updateCurrentPassword.bind(this);
this.updateNewPassword = this.updateNewPassword.bind(this);
this.updateConfirmPassword = this.updateConfirmPassword.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {currentPassword: '', newPassword: '', confirmPassword: ''};
}
submitPassword(e) {
e.preventDefault(); e.preventDefault();
var user = this.props.user; var user = this.props.user;
@@ -37,13 +47,13 @@ module.exports = React.createClass({
data.current_password = currentPassword; data.current_password = currentPassword;
data.new_password = newPassword; data.new_password = newPassword;
client.updatePassword(data, Client.updatePassword(data,
function() { function success() {
this.props.updateSection(''); this.props.updateSection('');
AsyncClient.getMe(); AsyncClient.getMe();
this.setState({currentPassword: '', newPassword: '', confirmPassword: ''}); this.setState({currentPassword: '', newPassword: '', confirmPassword: ''});
}.bind(this), }.bind(this),
function(err) { function fail(err) {
var state = this.getInitialState(); var state = this.getInitialState();
if (err.message) { if (err.message) {
state.serverError = err.message; state.serverError = err.message;
@@ -54,47 +64,49 @@ module.exports = React.createClass({
this.setState(state); this.setState(state);
}.bind(this) }.bind(this)
); );
}, }
updateCurrentPassword: function(e) { updateCurrentPassword(e) {
this.setState({currentPassword: e.target.value}); this.setState({currentPassword: e.target.value});
}, }
updateNewPassword: function(e) { updateNewPassword(e) {
this.setState({newPassword: e.target.value}); this.setState({newPassword: e.target.value});
}, }
updateConfirmPassword: function(e) { updateConfirmPassword(e) {
this.setState({confirmPassword: e.target.value}); this.setState({confirmPassword: e.target.value});
}, }
handleHistoryOpen: function() { handleHistoryOpen() {
$("#user_settings").modal('hide'); $('#user_settings').modal('hide');
}, }
handleDevicesOpen: function() { handleDevicesOpen() {
$("#user_settings").modal('hide'); $('#user_settings').modal('hide');
}, }
handleClose: function() { handleClose() {
$(this.getDOMNode()).find('.form-control').each(function() { $(React.findDOMNode(this)).find('.form-control').each(function resetValue() {
this.value = ''; this.value = '';
}); });
this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null});
this.props.updateTab('general'); this.props.updateTab('general');
}, }
componentDidMount: function() { componentDidMount() {
$('#user_settings').on('hidden.bs.modal', this.handleClose); $('#user_settings').on('hidden.bs.modal', this.handleClose);
}, }
componentWillUnmount: function() { componentWillUnmount() {
$('#user_settings').off('hidden.bs.modal', this.handleClose); $('#user_settings').off('hidden.bs.modal', this.handleClose);
this.props.updateSection(''); this.props.updateSection('');
}, }
getInitialState: function() { render() {
return {currentPassword: '', newPassword: '', confirmPassword: ''}; var serverError;
}, if (this.state.serverError) {
render: function() { serverError = this.state.serverError;
var serverError = this.state.serverError ? this.state.serverError : null; }
var passwordError = this.state.passwordError ? this.state.passwordError : null; var passwordError;
if (this.state.passwordError) {
passwordError = this.state.passwordError;
}
var updateSectionStatus; var updateSectionStatus;
var passwordSection; var passwordSection;
var self = this;
if (this.props.activeSection === 'password') { if (this.props.activeSection === 'password') {
var inputs = []; var inputs = [];
var submit = null; var submit = null;
@@ -104,7 +116,12 @@ module.exports = React.createClass({
<div className='form-group'> <div className='form-group'>
<label className='col-sm-5 control-label'>Current Password</label> <label className='col-sm-5 control-label'>Current Password</label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input className='form-control' type='password' onChange={this.updateCurrentPassword} value={this.state.currentPassword}/> <input
className='form-control'
type='password'
onChange={this.updateCurrentPassword}
value={this.state.currentPassword}
/>
</div> </div>
</div> </div>
); );
@@ -112,7 +129,12 @@ module.exports = React.createClass({
<div className='form-group'> <div className='form-group'>
<label className='col-sm-5 control-label'>New Password</label> <label className='col-sm-5 control-label'>New Password</label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input className='form-control' type='password' onChange={this.updateNewPassword} value={this.state.newPassword}/> <input
className='form-control'
type='password'
onChange={this.updateNewPassword}
value={this.state.newPassword}
/>
</div> </div>
</div> </div>
); );
@@ -120,7 +142,12 @@ module.exports = React.createClass({
<div className='form-group'> <div className='form-group'>
<label className='col-sm-5 control-label'>Retype New Password</label> <label className='col-sm-5 control-label'>Retype New Password</label>
<div className='col-sm-7'> <div className='col-sm-7'>
<input className='form-control' type='password' onChange={this.updateConfirmPassword} value={this.state.confirmPassword}/> <input
className='form-control'
type='password'
onChange={this.updateConfirmPassword}
value={this.state.confirmPassword}
/>
</div> </div>
</div> </div>
); );
@@ -134,11 +161,11 @@ module.exports = React.createClass({
); );
} }
updateSectionStatus = function(e) { updateSectionStatus = function resetSection(e) {
self.props.updateSection(''); this.props.updateSection('');
self.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null}); this.setState({currentPassword: '', newPassword: '', confirmPassword: '', serverError: null, passwordError: null});
e.preventDefault(); e.preventDefault();
}; }.bind(this);
passwordSection = ( passwordSection = (
<SettingItemMax <SettingItemMax
@@ -154,17 +181,27 @@ module.exports = React.createClass({
var describe; var describe;
if (this.props.user.auth_service === '') { if (this.props.user.auth_service === '') {
var d = new Date(this.props.user.last_password_update); var d = new Date(this.props.user.last_password_update);
var hour = d.getHours() % 12 ? String(d.getHours() % 12) : '12'; var hour = '12';
var min = d.getMinutes() < 10 ? '0' + d.getMinutes() : String(d.getMinutes()); if (d.getHours() % 12) {
var timeOfDay = d.getHours() >= 12 ? ' pm' : ' am'; hour = String(d.getHours() % 12);
}
var min = String(d.getMinutes());
if (d.getMinutes() < 10) {
min = '0' + d.getMinutes();
}
var timeOfDay = ' am';
if (d.getHours() >= 12) {
timeOfDay = ' pm';
}
describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay; describe = 'Last updated ' + Constants.MONTHS[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear() + ' at ' + hour + ':' + min + timeOfDay;
} else { } else {
describe = 'Log in done through GitLab'; describe = 'Log in done through GitLab';
} }
updateSectionStatus = function() { updateSectionStatus = function updateSection() {
self.props.updateSection('password'); this.props.updateSection('password');
}; }.bind(this);
passwordSection = ( passwordSection = (
<SettingItemMin <SettingItemMin
@@ -178,8 +215,20 @@ module.exports = React.createClass({
return ( return (
<div> <div>
<div className='modal-header'> <div className='modal-header'>
<button type='button' className='close' data-dismiss='modal' aria-label='Close'><span aria-hidden='true'>&times;</span></button> <button
<h4 className='modal-title' ref='title'><i className='modal-back'></i>Security Settings</h4> type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>&times;</span>
</button>
<h4
className='modal-title'
ref='title'
>
<i className='modal-back'></i>Security Settings
</h4>
</div> </div>
<div className='user-settings'> <div className='user-settings'>
<h3 className='tab-header'>Security Settings</h3> <h3 className='tab-header'>Security Settings</h3>
@@ -187,11 +236,38 @@ module.exports = React.createClass({
{passwordSection} {passwordSection}
<div className='divider-dark'/> <div className='divider-dark'/>
<br></br> <br></br>
<a data-toggle='modal' className='security-links theme' data-target='#access-history' href='#' onClick={this.handleHistoryOpen}><i className='fa fa-clock-o'></i>View Access History</a> <a
data-toggle='modal'
className='security-links theme'
data-target='#access-history'
href='#'
onClick={this.handleHistoryOpen}
>
<i className='fa fa-clock-o'></i>View Access History
</a>
<b> </b> <b> </b>
<a data-toggle='modal' className='security-links theme' data-target='#activity-log' href='#' onClick={this.handleDevicesOpen}><i className='fa fa-globe'></i>View and Logout of Active Sessions</a> <a
data-toggle='modal'
className='security-links theme'
data-target='#activity-log'
href='#'
onClick={this.handleDevicesOpen}
>
<i className='fa fa-globe'></i>View and Logout of Active Sessions
</a>
</div> </div>
</div> </div>
); );
} }
}); }
SecurityTab.defaultProps = {
user: {},
activeSection: ''
};
SecurityTab.propTypes = {
user: React.PropTypes.object,
activeSection: React.PropTypes.string,
updateSection: React.PropTypes.func,
updateTab: React.PropTypes.func
};

Просмотреть файл

@@ -2,35 +2,46 @@
// See License.txt for license information. // See License.txt for license information.
var Client = require('../utils/client.jsx'); var Client = require('../utils/client.jsx');
var utils = require('../utils/utils.jsx'); var Utils = require('../utils/utils.jsx');
module.exports = React.createClass({ export default class ViewImageModal extends React.Component {
displayName: 'ViewImageModal', constructor(props) {
propTypes: { super(props);
filenames: React.PropTypes.array,
modalId: React.PropTypes.string, this.canSetState = false;
channelId: React.PropTypes.string,
userId: React.PropTypes.string, this.loadImage = this.loadImage.bind(this);
startId: React.PropTypes.number this.handleNext = this.handleNext.bind(this);
}, this.handlePrev = this.handlePrev.bind(this);
canSetState: false, this.handleKeyPress = this.handleKeyPress.bind(this);
handleNext: function() { this.getPublicLink = this.getPublicLink.bind(this);
this.getPreviewImagePath = this.getPreviewImagePath.bind(this);
var loaded = [];
var progress = [];
for (var i = 0; i < this.props.filenames.length; i++) {
loaded.push(false);
progress.push(0);
}
this.state = {imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {}};
}
handleNext() {
var id = this.state.imgId + 1; var id = this.state.imgId + 1;
if (id > this.props.filenames.length - 1) { if (id > this.props.filenames.length - 1) {
id = 0; id = 0;
} }
this.setState({imgId: id}); this.setState({imgId: id});
this.loadImage(id); this.loadImage(id);
}, }
handlePrev: function() { handlePrev() {
var id = this.state.imgId - 1; var id = this.state.imgId - 1;
if (id < 0) { if (id < 0) {
id = this.props.filenames.length - 1; id = this.props.filenames.length - 1;
} }
this.setState({imgId: id}); this.setState({imgId: id});
this.loadImage(id); this.loadImage(id);
}, }
handleKeyPress: function handleKeyPress(e) { handleKeyPress(e) {
if (!e) { if (!e) {
return; return;
} else if (e.keyCode === 39) { } else if (e.keyCode === 39) {
@@ -38,11 +49,11 @@ module.exports = React.createClass({
} else if (e.keyCode === 37) { } else if (e.keyCode === 37) {
this.handlePrev(); this.handlePrev();
} }
}, }
componentWillReceiveProps: function(nextProps) { componentWillReceiveProps(nextProps) {
this.setState({imgId: nextProps.startId}); this.setState({imgId: nextProps.startId});
}, }
loadImage: function(id) { loadImage(id) {
var imgHeight = $(window).height() - 100; var imgHeight = $(window).height() - 100;
if (this.state.loaded[id] || this.state.images[id]) { if (this.state.loaded[id] || this.state.images[id]) {
$('.modal .modal-image .image-wrapper img').css('max-height', imgHeight); $('.modal .modal-image .image-wrapper img').css('max-height', imgHeight);
@@ -51,26 +62,25 @@ module.exports = React.createClass({
var filename = this.props.filenames[id]; var filename = this.props.filenames[id];
var fileInfo = utils.splitFileLocation(filename); var fileInfo = Utils.splitFileLocation(filename);
var fileType = utils.getFileType(fileInfo.ext); var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') { if (fileType === 'image') {
var self = this;
var img = new Image(); var img = new Image();
img.load(this.getPreviewImagePath(filename), img.load(this.getPreviewImagePath(filename),
function() { function load() {
var progress = self.state.progress; var progress = this.state.progress;
progress[id] = img.completedPercentage; progress[id] = img.completedPercentage;
self.setState({progress: progress}); this.setState({progress: progress});
}); }.bind(this));
img.onload = function onload(imgid) { img.onload = (function onload(imgid) {
return function onloadReturn() { return function onloadReturn() {
var loaded = self.state.loaded; var loaded = this.state.loaded;
loaded[imgid] = true; loaded[imgid] = true;
self.setState({loaded: loaded}); this.setState({loaded: loaded});
$(self.refs.image.getDOMNode()).css('max-height', imgHeight); $(React.findDOMNode(this.refs.image)).css('max-height', imgHeight);
}; }.bind(this);
}(id); }.bind(this)(id));
var images = this.state.images; var images = this.state.images;
images[id] = img; images[id] = img;
this.setState({images: images}); this.setState({images: images});
@@ -80,52 +90,51 @@ module.exports = React.createClass({
loaded[id] = true; loaded[id] = true;
this.setState({loaded: loaded}); this.setState({loaded: loaded});
} }
}, }
componentDidUpdate: function() { componentDidUpdate() {
if (this.state.loaded[this.state.imgId]) { if (this.state.loaded[this.state.imgId]) {
if (this.refs.imageWrap) { if (this.refs.imageWrap) {
$(this.refs.imageWrap.getDOMNode()).removeClass('default'); $(React.findDOMNode(this.refs.imageWrap)).removeClass('default');
} }
} }
}, }
componentDidMount: function() { componentDidMount() {
var self = this;
$('#' + this.props.modalId).on('shown.bs.modal', function onModalShow() { $('#' + this.props.modalId).on('shown.bs.modal', function onModalShow() {
self.setState({viewed: true}); this.setState({viewed: true});
self.loadImage(self.state.imgId); this.loadImage(this.state.imgId);
}); }.bind(this));
$(this.refs.modal.getDOMNode()).click(function onModalClick(e) { $(React.findDOMNode(this.refs.modal)).click(function onModalClick(e) {
if (e.target === this || e.target === self.refs.imageBody.getDOMNode()) { if (e.target === this || e.target === React.findDOMNode(this.refs.imageBody)) {
$('.image_modal').modal('hide'); $('.image_modal').modal('hide');
} }
}); }.bind(this));
$(this.refs.imageWrap.getDOMNode()).hover( $(React.findDOMNode(this.refs.imageWrap)).hover(
function onModalHover() { function onModalHover() {
$(self.refs.imageFooter.getDOMNode()).addClass('footer--show'); $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
}, function offModalHover() { }.bind(this), function offModalHover() {
$(self.refs.imageFooter.getDOMNode()).removeClass('footer--show'); $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
} }.bind(this)
); );
if (this.refs.previewArrowLeft) { if (this.refs.previewArrowLeft) {
$(this.refs.previewArrowLeft.getDOMNode()).hover( $(React.findDOMNode(this.refs.previewArrowLeft)).hover(
function onModalHover() { function onModalHover() {
$(self.refs.imageFooter.getDOMNode()).addClass('footer--show'); $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
}, function offModalHover() { }.bind(this), function offModalHover() {
$(self.refs.imageFooter.getDOMNode()).removeClass('footer--show'); $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
} }.bind(this)
); );
} }
if (this.refs.previewArrowRight) { if (this.refs.previewArrowRight) {
$(this.refs.previewArrowRight.getDOMNode()).hover( $(React.findDOMNode(this.refs.previewArrowRight)).hover(
function onModalHover() { function onModalHover() {
$(self.refs.imageFooter.getDOMNode()).addClass('footer--show'); $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
}, function offModalHover() { }.bind(this), function offModalHover() {
$(self.refs.imageFooter.getDOMNode()).removeClass('footer--show'); $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
} }.bind(this)
); );
} }
@@ -133,90 +142,93 @@ module.exports = React.createClass({
// keep track of whether or not this component is mounted so we can safely set the state asynchronously // keep track of whether or not this component is mounted so we can safely set the state asynchronously
this.canSetState = true; this.canSetState = true;
}, }
componentWillUnmount: function() { componentWillUnmount() {
this.canSetState = false; this.canSetState = false;
$(window).off('keyup', this.handleKeyPress); $(window).off('keyup', this.handleKeyPress);
}, }
getPublicLink: function() { getPublicLink() {
var data = {}; var data = {};
data.channel_id = this.props.channelId; data.channel_id = this.props.channelId;
data.user_id = this.props.userId; data.user_id = this.props.userId;
data.filename = this.props.filenames[this.state.imgId]; data.filename = this.props.filenames[this.state.imgId];
Client.getPublicLink(data, Client.getPublicLink(data,
function sucess(serverData) { function sucess(serverData) {
if (utils.isMobile()) { if (Utils.isMobile()) {
window.location.href = serverData.public_link; window.location.href = serverData.public_link;
} else { } else {
window.open(serverData.public_link); window.open(serverData.public_link);
} }
}, },
function error() { function error() {}
}
); );
}, }
getPreviewImagePath: function(filename) { getPreviewImagePath(filename) {
// Returns the path to a preview image that can be used to represent a file. // Returns the path to a preview image that can be used to represent a file.
var fileInfo = utils.splitFileLocation(filename); var fileInfo = Utils.splitFileLocation(filename);
var fileType = utils.getFileType(fileInfo.ext); var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') { if (fileType === 'image') {
// This is a temporary patch to fix issue with old files using absolute paths // This is a temporary patch to fix issue with old files using absolute paths
if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) { if (fileInfo.path.indexOf('/api/v1/files/get') !== -1) {
fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1]; fileInfo.path = fileInfo.path.split('/api/v1/files/get')[1];
} }
fileInfo.path = utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path; fileInfo.path = Utils.getWindowLocationOrigin() + '/api/v1/files/get' + fileInfo.path;
return fileInfo.path + '_preview.jpg'; return fileInfo.path + '_preview.jpg';
} }
// only images have proper previews, so just use a placeholder icon for non-images // only images have proper previews, so just use a placeholder icon for non-images
return utils.getPreviewImagePathForFileType(fileType); return Utils.getPreviewImagePathForFileType(fileType);
}, }
getInitialState: function() { render() {
var loaded = [];
var progress = [];
for (var i = 0; i < this.props.filenames.length; i++) {
loaded.push(false);
progress.push(0);
}
return {imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {}};
},
render: function() {
if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) { if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) {
return <div/>; return <div/>;
} }
var filename = this.props.filenames[this.state.imgId]; var filename = this.props.filenames[this.state.imgId];
var fileUrl = utils.getFileUrl(filename); var fileUrl = Utils.getFileUrl(filename);
var name = decodeURIComponent(utils.getFileName(filename)); var name = decodeURIComponent(Utils.getFileName(filename));
var content; var content;
var bgClass = ''; var bgClass = '';
if (this.state.loaded[this.state.imgId]) { if (this.state.loaded[this.state.imgId]) {
var fileInfo = utils.splitFileLocation(filename); var fileInfo = Utils.splitFileLocation(filename);
var fileType = utils.getFileType(fileInfo.ext); var fileType = Utils.getFileType(fileInfo.ext);
if (fileType === 'image') { if (fileType === 'image') {
// image files just show a preview of the file // image files just show a preview of the file
content = ( content = (
<a href={fileUrl} target='_blank'> <a
<img ref='image' src={this.getPreviewImagePath(filename)}/> href={fileUrl}
target='_blank'
>
<img
ref='image'
src={this.getPreviewImagePath(filename)}
/>
</a> </a>
); );
} else { } else {
// non-image files include a section providing details about the file // non-image files include a section providing details about the file
var infoString = 'File type ' + fileInfo.ext.toUpperCase(); var infoString = 'File type ' + fileInfo.ext.toUpperCase();
if (this.state.fileSizes[filename] && this.state.fileSizes[filename] >= 0) { if (this.state.fileSizes[filename] && this.state.fileSizes[filename] >= 0) {
infoString += ', Size ' + utils.fileSizeToString(this.state.fileSizes[filename]); infoString += ', Size ' + Utils.fileSizeToString(this.state.fileSizes[filename]);
} }
content = ( content = (
<div className='file-details__container'> <div className='file-details__container'>
<a className={'file-details__preview'} href={fileUrl} target='_blank'> <a
className={'file-details__preview'}
href={fileUrl}
target='_blank'
>
<span className='file-details__preview-helper' /> <span className='file-details__preview-helper' />
<img ref='image' src={this.getPreviewImagePath(filename)} /> <img
ref='image'
src={this.getPreviewImagePath(filename)}
/>
</a> </a>
<div className='file-details'> <div className='file-details'>
<div className='file-details__name'>{name}</div> <div className='file-details__name'>{name}</div>
@@ -228,19 +240,16 @@ module.exports = React.createClass({
// asynchronously request the actual size of this file // asynchronously request the actual size of this file
if (!(filename in this.state.fileSizes)) { if (!(filename in this.state.fileSizes)) {
var self = this;
Client.getFileInfo( Client.getFileInfo(
filename, filename,
function(data) { function success(data) {
if (self.canSetState) { if (this.canSetState) {
var fileSizes = self.state.fileSizes; var fileSizes = this.state.fileSizes;
fileSizes[filename] = parseInt(data["size"], 10); fileSizes[filename] = parseInt(data.size, 10);
self.setState(fileSizes); this.setState(fileSizes);
} }
}, }.bind(this),
function(err) { function fail() {}
}
); );
} }
} }
@@ -250,14 +259,22 @@ module.exports = React.createClass({
if (percentage) { if (percentage) {
content = ( content = (
<div> <div>
<img className='loader-image' src='/static/images/load.gif' /> <img
<span className='loader-percent' >{'Previewing ' + percentage + '%'}</span> className='loader-image'
src='/static/images/load.gif'
/>
<span className='loader-percent'>
{'Previewing ' + percentage + '%'}
</span>
</div> </div>
); );
} else { } else {
content = ( content = (
<div> <div>
<img className='loader-image' src='/static/images/load.gif' /> <img
className='loader-image'
src='/static/images/load.gif'
/>
</div> </div>
); );
} }
@@ -268,7 +285,14 @@ module.exports = React.createClass({
if (config.AllowPublicLink) { if (config.AllowPublicLink) {
publicLink = ( publicLink = (
<div> <div>
<a href='#' className='public-link text' data-title='Public Image' onClick={this.getPublicLink}>Get Public Link</a> <a
href='#'
className='public-link text'
data-title='Public Image'
onClick={this.getPublicLink}
>
Get Public Link
</a>
<span className='text'> | </span> <span className='text'> | </span>
</div> </div>
); );
@@ -299,18 +323,43 @@ module.exports = React.createClass({
} }
return ( return (
<div className='modal fade image_modal' ref='modal' id={this.props.modalId} tabIndex='-1' role='dialog' aria-hidden='true'> <div
className='modal fade image_modal'
ref='modal'
id={this.props.modalId}
tabIndex='-1'
role='dialog'
aria-hidden='true'
>
<div className='modal-dialog modal-image'> <div className='modal-dialog modal-image'>
<div className='modal-content image-content'> <div className='modal-content image-content'>
<div ref='imageBody' className='modal-body image-body'> <div
<div ref='imageWrap' className={'image-wrapper default ' + bgClass}> ref='imageBody'
<div className='modal-close' data-dismiss='modal'></div> className='modal-body image-body'
>
<div
ref='imageWrap'
className={'image-wrapper default ' + bgClass}
>
<div
className='modal-close'
data-dismiss='modal'
/>
{content} {content}
<div ref='imageFooter' className='modal-button-bar'> <div
ref='imageFooter'
className='modal-button-bar'
>
<span className='pull-left text'>{'File ' + (this.state.imgId + 1) + ' of ' + this.props.filenames.length}</span> <span className='pull-left text'>{'File ' + (this.state.imgId + 1) + ' of ' + this.props.filenames.length}</span>
<div className='image-links'> <div className='image-links'>
{publicLink} {publicLink}
<a href={fileUrl} download={name} className='text'>Download</a> <a
href={fileUrl}
download={name}
className='text'
>
Download
</a>
</div> </div>
</div> </div>
</div> </div>
@@ -322,4 +371,19 @@ module.exports = React.createClass({
</div> </div>
); );
} }
}); }
ViewImageModal.defaultProps = {
filenames: [],
modalId: '',
channelId: '',
userId: '',
startId: 0
};
ViewImageModal.propTypes = {
filenames: React.PropTypes.array,
modalId: React.PropTypes.string,
channelId: React.PropTypes.string,
userId: React.PropTypes.string,
startId: React.PropTypes.number
};