New add channel modal using react-bootstrap.

Этот коммит содержится в:
Christopher Speller
2015-09-14 13:56:58 -04:00
родитель b2378f433e
Коммит 0ea0233c50
27 изменённых файлов: 18348 добавлений и 625 удалений

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

@@ -78,9 +78,10 @@ travis:
mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
@sed -i'.bak' 's|react-with-addons-0.13.1.js|react-with-addons-0.13.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|react-with-addons-0.13.3.js|react-with-addons-0.13.3.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|jquery-1.11.1.js|jquery-1.11.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bootstrap-3.3.1.js|bootstrap-3.3.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|react-bootstrap-0.25.1.js|react-bootstrap-0.25.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|perfect-scrollbar.js|perfect-scrollbar.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
rm $(DIST_PATH)/web/templates/*.bak
@@ -231,9 +232,10 @@ dist: install
mv $(DIST_PATH)/web/static/js/bundle.min.js $(DIST_PATH)/web/static/js/bundle-$(BUILD_NUMBER).min.js
@sed -i'.bak' 's|react-with-addons-0.13.1.js|react-with-addons-0.13.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|react-with-addons-0.13.3.js|react-with-addons-0.13.3.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|jquery-1.11.1.js|jquery-1.11.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bootstrap-3.3.1.js|bootstrap-3.3.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bootstrap-3.3.5.js|bootstrap-3.3.5.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|react-bootstrap-0.25.1.js|react-bootstrap-0.25.1.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|perfect-scrollbar.js|perfect-scrollbar.min.js|g' $(DIST_PATH)/web/templates/head.html
@sed -i'.bak' 's|bundle.js|bundle-$(BUILD_NUMBER).min.js|g' $(DIST_PATH)/web/templates/head.html
rm $(DIST_PATH)/web/templates/*.bak

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

@@ -18,7 +18,8 @@
"es6": true
},
"globals": {
"React": false
"React": false,
"ReactBootstrap": false
},
"rules": {
"comma-dangle": [2, "never"],

177
web/react/components/change_url_modal.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,177 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Modal = ReactBootstrap.Modal;
var Utils = require('../utils/utils.jsx');
export default class ChangeUrlModal extends React.Component {
constructor(props) {
super(props);
this.onURLChanged = this.onURLChanged.bind(this);
this.doSubmit = this.doSubmit.bind(this);
this.doCancel = this.doCancel.bind(this);
this.state = {
currentURL: props.currentURL,
urlError: '',
userEdit: false
};
}
componentWillReceiveProps(nextProps) {
// This check prevents the url being deleted when we re-render
// because of user status check
if (!this.state.userEdit) {
this.setState({
currentURL: nextProps.currentURL
});
}
}
componentDidUpdate(prevProps) {
if (this.props.show === true && prevProps.show === false) {
React.findDOMNode(this.refs.urlinput).select();
}
}
onURLChanged(e) {
const url = e.target.value.trim();
this.setState({currentURL: url.replace(/[^A-Za-z0-9-_]/g, '').toLowerCase(), userEdit: true});
}
getURLError(url) {
let error = []; //eslint-disable-line prefer-const
if (url.length < 2) {
error.push(<span key='error1'>{'Must be longer than two characters'}<br/></span>);
}
if (url.charAt(0) === '-' || url.charAt(0) === '_') {
error.push(<span key='error2'>{'Must start with a letter or number'}<br/></span>);
}
if (url.length > 1 && (url.charAt(url.length - 1) === '-' || url.charAt(url.length - 1) === '_')) {
error.push(<span key='error3'>{'Must end with a letter or number'}<br/></span>);
}
if (url.indexOf('__') > -1) {
error.push(<span key='error4'>{'Can not contain two underscores in a row.'}<br/></span>);
}
// In case of error we don't detect
if (error.length === 0) {
error.push(<span key='errorlast'>{'Invalid URL'}<br/></span>);
}
return error;
}
doSubmit(e) {
e.preventDefault();
const url = React.findDOMNode(this.refs.urlinput).value;
const cleanedURL = Utils.cleanUpUrlable(url);
if (cleanedURL !== url || url.length < 2 || url.indexOf('__') > -1) {
this.setState({urlError: this.getURLError(url)});
return;
}
this.setState({urlError: '', userEdit: false});
this.props.onModalSubmit(url);
}
doCancel() {
this.setState({urlError: '', userEdit: false});
this.props.onModalDismissed();
}
render() {
let urlClass = 'input-group input-group--limit';
let urlError = null;
let serverError = null;
if (this.state.urlError) {
urlClass += ' has-error';
urlError = (<p className='input__help error'>{this.state.urlError}</p>);
}
if (this.props.serverError) {
serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>;
}
const fullTeamUrl = Utils.getTeamURLFromAddressBar();
const teamURL = Utils.getShortenedTeamURL();
return (
<Modal
show={this.props.show}
onHide={this.doCancel}
>
<Modal.Header closeButton={true}>
<Modal.Title>{this.props.title}</Modal.Title>
</Modal.Header>
<form
role='form'
className='form-horizontal'
>
<Modal.Body>
<div className='modal-intro'>{this.props.description}</div>
<div className='form-group'>
<label className='col-sm-2 form__label control-label'>{this.props.urlLabel}</label>
<div className='col-sm-10'>
<div className={urlClass}>
<span
data-toggle='tooltip'
title={fullTeamUrl}
className='input-group-addon'
>
{teamURL}
</span>
<input
type='text'
ref='urlinput'
className='form-control'
maxLength='22'
onChange={this.onURLChanged}
value={this.state.currentURL}
autoFocus={true}
tabIndex='1'
/>
</div>
{urlError}
{serverError}
</div>
</div>
</Modal.Body>
<Modal.Footer>
<button
type='button'
className='btn btn-default'
onClick={this.doCancel}
>
{'Close'}
</button>
<button
onClick={this.doSubmit}
type='submit'
className='btn btn-primary'
tabIndex='2'
>
{this.props.submitButtonText}
</button>
</Modal.Footer>
</form>
</Modal>
);
}
}
ChangeUrlModal.defaultProps = {
show: false,
title: 'Change URL',
desciption: '',
urlLabel: 'URL',
submitButtonText: 'Submit',
currentURL: '',
serverError: ''
};
ChangeUrlModal.propTypes = {
show: React.PropTypes.bool.isRequired,
title: React.PropTypes.string,
description: React.PropTypes.string,
urlLabel: React.PropTypes.string,
submitButtonText: React.PropTypes.string,
currentURL: React.PropTypes.string,
serverError: React.PropTypes.string,
onModalSubmit: React.PropTypes.func.isRequired,
onModalDismissed: React.PropTypes.func.isRequired
};

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

@@ -53,7 +53,7 @@ export default class FileUpload extends React.Component {
}
// generate a unique id that can be used by other components to refer back to this upload
var clientId = utils.generateId();
let clientId = utils.generateId();
// prepare data to be uploaded
var formData = new FormData();

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

@@ -1,211 +0,0 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
var asyncClient = require('../utils/async_client.jsx');
var UserStore = require('../stores/user_store.jsx');
export default class NewChannelModal extends React.Component {
constructor() {
super();
this.handleSubmit = this.handleSubmit.bind(this);
this.displayNameKeyUp = this.displayNameKeyUp.bind(this);
this.handleClose = this.handleClose.bind(this);
this.state = {channelType: ''};
}
handleSubmit(e) {
e.preventDefault();
var channel = {};
var state = {serverError: ''};
channel.display_name = React.findDOMNode(this.refs.display_name).value.trim();
if (!channel.display_name) {
state.displayNameError = 'This field is required';
state.inValid = true;
} else if (channel.display_name.length > 22) {
state.displayNameError = 'This field must be less than 22 characters';
state.inValid = true;
} else {
state.displayNameError = '';
}
channel.name = React.findDOMNode(this.refs.channel_name).value.trim();
if (!channel.name) {
state.nameError = 'This field is required';
state.inValid = true;
} else if (channel.name.length > 22) {
state.nameError = 'This field must be less than 22 characters';
state.inValid = true;
} else {
var cleanedName = utils.cleanUpUrlable(channel.name);
if (cleanedName !== channel.name) {
state.nameError = "Must be lowercase alphanumeric characters, allowing '-' but not starting or ending with '-'";
state.inValid = true;
} else {
state.nameError = '';
}
}
this.setState(state);
if (state.inValid) {
return;
}
var cu = UserStore.getCurrentUser();
channel.team_id = cu.team_id;
channel.description = React.findDOMNode(this.refs.channel_desc).value.trim();
channel.type = this.state.channelType;
client.createChannel(channel,
function success(data) {
$(React.findDOMNode(this.refs.modal)).modal('hide');
asyncClient.getChannel(data.id);
utils.switchChannel(data);
React.findDOMNode(this.refs.display_name).value = '';
React.findDOMNode(this.refs.channel_name).value = '';
React.findDOMNode(this.refs.channel_desc).value = '';
}.bind(this),
function error(err) {
state.serverError = err.message;
state.inValid = true;
this.setState(state);
}.bind(this)
);
}
displayNameKeyUp() {
var displayName = React.findDOMNode(this.refs.display_name).value.trim();
var channelName = utils.cleanUpUrlable(displayName);
React.findDOMNode(this.refs.channel_name).value = channelName;
}
componentDidMount() {
var self = this;
$(React.findDOMNode(this.refs.modal)).on('show.bs.modal', function onModalShow(e) {
var button = e.relatedTarget;
self.setState({channelType: $(button).attr('data-channeltype')});
});
$(React.findDOMNode(this.refs.modal)).on('hidden.bs.modal', this.handleClose);
}
componentWillUnmount() {
$(React.findDOMNode(this.refs.modal)).off('hidden.bs.modal', this.handleClose);
}
handleClose() {
$(React.findDOMNode(this)).find('.form-control').each(function clearForms() {
this.value = '';
});
this.setState({channelType: '', displayNameError: '', nameError: '', serverError: '', inValid: false});
}
render() {
var displayNameError = null;
var nameError = null;
var serverError = null;
var displayNameClass = 'form-group';
var nameClass = 'form-group';
if (this.state.displayNameError) {
displayNameError = <label className='control-label'>{this.state.displayNameError}</label>;
displayNameClass += ' has-error';
}
if (this.state.nameError) {
nameError = <label className='control-label'>{this.state.nameError}</label>;
nameClass += ' has-error';
}
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var channelTerm = 'Channel';
if (this.state.channelType === 'P') {
channelTerm = 'Group';
}
return (
<div
className='modal fade'
id='new_channel'
ref='modal'
tabIndex='-1'
role='dialog'
aria-hidden='true'
>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<button
type='button'
className='close'
data-dismiss='modal'
>
<span aria-hidden='true'>&times;</span>
<span className='sr-only'>Cancel</span>
</button>
<h4 className='modal-title'>New {channelTerm}</h4>
</div>
<form role='form'>
<div className='modal-body'>
<div className={displayNameClass}>
<label className='control-label'>Display Name</label>
<input
onKeyUp={this.displayNameKeyUp}
type='text'
ref='display_name'
className='form-control'
placeholder='Enter display name'
maxLength='22'
/>
{displayNameError}
</div>
<div className={nameClass}>
<label className='control-label'>Handle</label>
<input
type='text'
className='form-control'
ref='channel_name'
placeholder="lowercase alphanumeric's only"
maxLength='22'
/>
{nameError}
</div>
<div className='form-group'>
<label className='control-label'>Description</label>
<textarea
className='form-control no-resize'
ref='channel_desc'
rows='3'
placeholder='Description'
maxLength='1024'
/>
</div>
{serverError}
</div>
<div className='modal-footer'>
<button
type='button'
className='btn btn-default'
data-dismiss='modal'
>
Cancel
</button>
<button
onClick={this.handleSubmit}
type='submit'
className='btn btn-primary'
>
Create New {channelTerm}
</button>
</div>
</form>
</div>
</div>
</div>
);
}
}

206
web/react/components/new_channel_flow.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,206 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Utils = require('../utils/utils.jsx');
var AsyncClient = require('../utils/async_client.jsx');
var Client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx');
var NewChannelModal = require('./new_channel_modal.jsx');
var ChangeURLModal = require('./change_url_modal.jsx');
const SHOW_NEW_CHANNEL = 1;
const SHOW_EDIT_URL = 2;
const SHOW_EDIT_URL_THEN_COMPLETE = 3;
export default class NewChannelFlow extends React.Component {
constructor(props) {
super(props);
this.doSubmit = this.doSubmit.bind(this);
this.typeSwitched = this.typeSwitched.bind(this);
this.urlChangeRequested = this.urlChangeRequested.bind(this);
this.urlChangeSubmitted = this.urlChangeSubmitted.bind(this);
this.urlChangeDismissed = this.urlChangeDismissed.bind(this);
this.channelDataChanged = this.channelDataChanged.bind(this);
this.state = {
serverError: '',
channelType: 'O',
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
channelDescription: '',
nameModified: false
};
}
componentWillReceiveProps(nextProps) {
// If we are being shown, grab channel type from props and clear
if (nextProps.show === true && this.props.show === false) {
this.setState({
serverError: '',
channelType: nextProps.channelType,
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
channelDescription: '',
nameModified: false
});
}
}
doSubmit() {
var channel = {};
channel.display_name = this.state.channelDisplayName;
if (!channel.display_name) {
this.setState({serverError: 'Invalid Channel Name'});
return;
}
channel.name = this.state.channelName;
if (channel.name.length < 2) {
this.setState({flowState: SHOW_EDIT_URL_THEN_COMPLETE});
return;
}
const cu = UserStore.getCurrentUser();
channel.team_id = cu.team_id;
channel.description = this.state.channelDescription;
channel.type = this.state.channelType;
Client.createChannel(channel,
(data) => {
this.props.onModalDismissed();
AsyncClient.getChannel(data.id);
Utils.switchChannel(data);
},
(err) => {
if (err.message === 'Name must be 2 or more lowercase alphanumeric characters') {
this.setState({flowState: SHOW_EDIT_URL_THEN_COMPLETE});
}
if (err.message === 'A channel with that handle already exists') {
this.setState({serverError: 'A channel with that URL already exists'});
return;
}
this.setState({serverError: err.message});
}
);
}
typeSwitched() {
if (this.state.channelType === 'P') {
this.setState({channelType: 'O'});
} else {
this.setState({channelType: 'P'});
}
}
urlChangeRequested() {
this.setState({flowState: SHOW_EDIT_URL});
}
urlChangeSubmitted(newURL) {
if (this.state.flowState === SHOW_EDIT_URL_THEN_COMPLETE) {
this.setState({channelName: newURL, nameModified: true}, this.doSubmit);
} else {
this.setState({flowState: SHOW_NEW_CHANNEL, serverError: '', channelName: newURL, nameModified: true});
}
}
urlChangeDismissed() {
this.setState({flowState: SHOW_NEW_CHANNEL});
}
channelDataChanged(data) {
this.setState({
channelDisplayName: data.displayName,
channelDescription: data.description
});
if (!this.state.nameModified) {
this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
}
}
render() {
const channelData = {
name: this.state.channelName,
displayName: this.state.channelDisplayName,
description: this.state.channelDescription
};
let showChannelModal = false;
let showGroupModal = false;
let showChangeURLModal = false;
let changeURLTitle = '';
let changeURLSubmitButtonText = '';
let channelTerm = '';
// Only listen to flow state if we are being shown
if (this.props.show) {
switch (this.state.flowState) {
case SHOW_NEW_CHANNEL:
if (this.state.channelType === 'O') {
showChannelModal = true;
channelTerm = 'Channel';
} else {
showGroupModal = true;
channelTerm = 'Group';
}
break;
case SHOW_EDIT_URL:
showChangeURLModal = true;
changeURLTitle = 'Change ' + channelTerm + ' URL';
changeURLSubmitButtonText = 'Change ' + channelTerm + ' URL';
break;
case SHOW_EDIT_URL_THEN_COMPLETE:
showChangeURLModal = true;
changeURLTitle = 'Set ' + channelTerm + ' URL';
changeURLSubmitButtonText = 'Create ' + channelTerm;
break;
}
}
return (
<span>
<NewChannelModal
show={showChannelModal}
channelType={'O'}
channelData={channelData}
serverError={this.state.serverError}
onSubmitChannel={this.doSubmit}
onModalDismissed={this.props.onModalDismissed}
onTypeSwitched={this.typeSwitched}
onChangeURLPressed={this.urlChangeRequested}
onDataChanged={this.channelDataChanged}
/>
<NewChannelModal
show={showGroupModal}
channelType={'P'}
channelData={channelData}
serverError={this.state.serverError}
onSubmitChannel={this.doSubmit}
onModalDismissed={this.props.onModalDismissed}
onTypeSwitched={this.typeSwitched}
onChangeURLPressed={this.urlChangeRequested}
onDataChanged={this.channelDataChanged}
/>
<ChangeURLModal
show={showChangeURLModal}
title={changeURLTitle}
description={'Some characters are not allowed in URLs and may be removed.'}
urlLabel={channelTerm + ' URL'}
submitButtonText={changeURLSubmitButtonText}
currentURL={this.state.channelName}
serverError={this.state.serverError}
onModalSubmit={this.urlChangeSubmitted}
onModalDismissed={this.urlChangeDismissed}
/>
</span>
);
}
}
NewChannelFlow.defaultProps = {
show: false,
channelType: 'O'
};
NewChannelFlow.propTypes = {
show: React.PropTypes.bool.isRequired,
channelType: React.PropTypes.string.isRequired,
onModalDismissed: React.PropTypes.func.isRequired
};

198
web/react/components/new_channel_modal.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,198 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
const Utils = require('../utils/utils.jsx');
var Modal = ReactBootstrap.Modal;
export default class NewChannelModal extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleChange = this.handleChange.bind(this);
this.state = {
displayNameError: ''
};
}
componentWillReceiveProps(nextProps) {
if (nextProps.show === true && this.props.show === false) {
this.setState({
displayNameError: ''
});
}
}
handleSubmit(e) {
e.preventDefault();
const displayName = React.findDOMNode(this.refs.display_name).value.trim();
if (displayName.length < 1) {
this.setState({displayNameError: 'This field is required'});
return;
}
this.props.onSubmitChannel();
}
handleChange() {
const newData = {
displayName: React.findDOMNode(this.refs.display_name).value,
description: React.findDOMNode(this.refs.channel_desc).value
};
this.props.onDataChanged(newData);
}
render() {
var displayNameError = null;
var serverError = null;
var displayNameClass = 'form-group';
if (this.state.displayNameError) {
displayNameError = <p className='input__help error'>{this.state.displayNameError}</p>;
displayNameClass += ' has-error';
}
if (this.props.serverError) {
serverError = <div className='form-group has-error'><p className='input__help error'>{this.props.serverError}</p></div>;
}
var channelTerm = '';
var channelSwitchText = '';
switch (this.props.channelType) {
case 'P':
channelTerm = 'Group';
channelSwitchText = (
<div className='modal-intro'>
{'Create a new private group with restricted membership. '}
<a
href='#'
onClick={this.props.onTypeSwitched}
>
{'Create a public channel'}
</a>
</div>
);
break;
case 'O':
channelTerm = 'Channel';
channelSwitchText = (
<div className='modal-intro'>
{'Create a new public channel anyone can join. '}
<a
href='#'
onClick={this.props.onTypeSwitched}
>
{'Create a private group'}
</a>
</div>
);
break;
}
const prettyTeamURL = Utils.getShortenedTeamURL();
return (
<span>
<Modal
show={this.props.show}
onHide={this.props.onModalDismissed}
>
<Modal.Header closeButton={true}>
<Modal.Title>{'New ' + channelTerm}</Modal.Title>
</Modal.Header>
<form
role='form'
className='form-horizontal'
>
<Modal.Body>
<div>
{channelSwitchText}
</div>
<div className={displayNameClass}>
<label className='col-sm-2 form__label control-label'>{'Name'}</label>
<div className='col-sm-10'>
<input
onChange={this.handleChange}
type='text'
ref='display_name'
className='form-control'
placeholder='Ex: "Bugs", "Marketing", "办公室恋情"'
maxLength='22'
value={this.props.channelData.displayName}
autoFocus={true}
tabIndex='1'
/>
{displayNameError}
<p className='input__help'>
{'Channel URL: ' + prettyTeamURL + this.props.channelData.name + ' ('}
<a
href='#'
onClick={this.props.onChangeURLPressed}
>
{'change this URL'}
</a>
{')'}
</p>
</div>
</div>
<div className='form-group less'>
<div className='col-sm-2'>
<label className='form__label control-label'>{'Description'}</label>
<label className='form__label light'>{'(optional)'}</label>
</div>
<div className='col-sm-10'>
<textarea
className='form-control no-resize'
ref='channel_desc'
rows='4'
placeholder='Description'
maxLength='1024'
value={this.props.channelData.description}
onChange={this.handleChange}
tabIndex='2'
/>
<p className='input__help'>
{'The purpose of your channel. To help others decide whether to join.'}
</p>
{serverError}
</div>
</div>
</Modal.Body>
<Modal.Footer>
<button
type='button'
className='btn btn-default'
onClick={this.props.onModalDismissed}
>
{'Cancel'}
</button>
<button
onClick={this.handleSubmit}
type='submit'
className='btn btn-primary'
tabIndex='3'
>
{'Create New ' + channelTerm}
</button>
</Modal.Footer>
</form>
</Modal>
</span>
);
}
}
NewChannelModal.defaultProps = {
show: false,
channelType: 'O',
serverError: ''
};
NewChannelModal.propTypes = {
show: React.PropTypes.bool.isRequired,
channelType: React.PropTypes.string.isRequired,
channelData: React.PropTypes.object.isRequired,
serverError: React.PropTypes.string,
onSubmitChannel: React.PropTypes.func.isRequired,
onModalDismissed: React.PropTypes.func.isRequired,
onTypeSwitched: React.PropTypes.func.isRequired,
onChangeURLPressed: React.PropTypes.func.isRequired,
onDataChanged: React.PropTypes.func.isRequired
};

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

@@ -12,6 +12,7 @@ var Utils = require('../utils/utils.jsx');
var SidebarHeader = require('./sidebar_header.jsx');
var SearchBox = require('./search_bar.jsx');
var Constants = require('../utils/constants.jsx');
var NewChannelFlow = require('./new_channel_flow.jsx');
export default class Sidebar extends React.Component {
constructor(props) {
@@ -28,6 +29,7 @@ export default class Sidebar extends React.Component {
this.createChannelElement = this.createChannelElement.bind(this);
this.state = this.getStateFromStores();
this.state.modal = '';
this.state.loadingDMChannel = -1;
}
getStateFromStores() {
@@ -473,8 +475,18 @@ export default class Sidebar extends React.Component {
);
}
let showChannelModal = false;
if (this.state.modal !== '') {
showChannelModal = true;
}
return (
<div>
<NewChannelFlow
show={showChannelModal}
channelType={this.state.modal}
onModalDismissed={() => this.setState({modal: ''})}
/>
<SidebarHeader
teamDisplayName={this.props.teamDisplayName}
teamType={this.props.teamType}
@@ -508,11 +520,9 @@ export default class Sidebar extends React.Component {
<a
className='add-channel-btn'
href='#'
data-toggle='modal'
data-target='#new_channel'
data-channeltype='O'
onClick={() => this.setState({modal: 'O'})}
>
+
{'+'}
</a>
</h4>
</li>
@@ -537,11 +547,9 @@ export default class Sidebar extends React.Component {
<a
className='add-channel-btn'
href='#'
data-toggle='modal'
data-target='#new_channel'
data-channeltype='P'
onClick={() => this.setState({modal: 'P'})}
>
+
{'+'}
</a>
</h4>
</li>

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

@@ -7,7 +7,6 @@
"flux": "2.1.1",
"keymirror": "0.1.1",
"object-assign": "3.0.0",
"react": "0.13.3",
"react-zeroclipboard-mixin": "0.1.0",
"twemoji": "1.4.1"
},

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

@@ -17,7 +17,6 @@ var RenameChannelModal = require('../components/rename_channel_modal.jsx');
var EditPostModal = require('../components/edit_post_modal.jsx');
var DeletePostModal = require('../components/delete_post_modal.jsx');
var MoreChannelsModal = require('../components/more_channels.jsx');
var NewChannelModal = require('../components/new_channel.jsx');
var PostDeletedModal = require('../components/post_deleted_modal.jsx');
var ChannelNotificationsModal = require('../components/channel_notifications.jsx');
var UserSettingsModal = require('../components/user_settings_modal.jsx');
@@ -153,11 +152,6 @@ function setupChannelPage(teamName, teamType, teamId, channelName, channelId) {
document.getElementById('direct_channel_modal')
);
React.render(
<NewChannelModal />,
document.getElementById('new_channel_modal')
);
React.render(
<PostListContainer />,
document.getElementById('post-list')

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

@@ -1127,3 +1127,14 @@ export function importSlack(file, success, error) {
client.importSlack(formData, success, error);
}
export function getTeamURLFromAddressBar() {
return window.location.href.split('/channels')[0];
}
export function getShortenedTeamURL() {
const teamURL = getTeamURLFromAddressBar();
if (teamURL.length > 24) {
return teamURL.substring(0, 10) + '...' + teamURL.substring(teamURL.length - 12, teamURL.length - 1) + '/';
}
}

33
web/sass-files/sass/partials/_forms.scss Обычный файл
Просмотреть файл

@@ -0,0 +1,33 @@
.form-horizontal {
.modal-intro {
margin: -10px 0 30px;
}
.form__label {
text-align: left;
padding-right: 3px;
font-weight: bold;
font-size: 1.1em;
&.light {
color: #999;
font-size: 1.05em;
font-style: italic;
padding-top: 2px;
}
}
.input__help {
color: #777;
margin: 10px 0 0 10px;
&.error {
color: #a94442;
}
}
.form-control {
font-weight: normal;
}
.form-group {
margin-bottom: 25px;
&.less {
margin-bottom: 10px;
}
}
}

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

@@ -28,6 +28,7 @@
@import "partials/videos";
@import "partials/settings";
@import "partials/modal";
@import "partials/forms";
@import "partials/mentions";
@import "partials/command-box";
@import "partials/error";

5
web/static/css/bootstrap-3.3.1.min.css поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

5
web/static/css/bootstrap-3.3.5.min.css поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

7
web/static/js/bootstrap-3.3.1.min.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Разница между файлами не показана из-за своего большого размера Загрузить разницу

7
web/static/js/bootstrap-3.3.5.min.js поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

1024
web/static/js/jasny-bootstrap.js Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

6
web/static/js/jasny-bootstrap.min.js поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

15678
web/static/js/react-bootstrap-0.25.1.js Обычный файл

Разница между файлами не показана из-за своего большого размера Загрузить разницу

14
web/static/js/react-bootstrap-0.25.1.min.js поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

16
web/static/js/react-with-addons-0.13.1.min.js поставляемый

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@@ -1,5 +1,5 @@
/**
* React (with addons) v0.13.1
* React (with addons) v0.13.3
*/
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.React = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
/**
@@ -710,7 +710,9 @@ var isUnitlessNumber = {
columnCount: true,
flex: true,
flexGrow: true,
flexPositive: true,
flexShrink: true,
flexNegative: true,
fontWeight: true,
lineClamp: true,
lineHeight: true,
@@ -723,7 +725,9 @@ var isUnitlessNumber = {
// SVG-related properties
fillOpacity: true,
strokeOpacity: true
strokeDashoffset: true,
strokeOpacity: true,
strokeWidth: true
};
/**
@@ -3788,6 +3792,7 @@ var HTMLDOMPropertyConfig = {
headers: null,
height: MUST_USE_ATTRIBUTE,
hidden: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
high: null,
href: null,
hrefLang: null,
htmlFor: null,
@@ -3798,6 +3803,7 @@ var HTMLDOMPropertyConfig = {
lang: null,
list: MUST_USE_ATTRIBUTE,
loop: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
low: null,
manifest: MUST_USE_ATTRIBUTE,
marginHeight: null,
marginWidth: null,
@@ -3812,6 +3818,7 @@ var HTMLDOMPropertyConfig = {
name: null,
noValidate: HAS_BOOLEAN_VALUE,
open: HAS_BOOLEAN_VALUE,
optimum: null,
pattern: null,
placeholder: null,
poster: null,
@@ -3825,6 +3832,7 @@ var HTMLDOMPropertyConfig = {
rowSpan: null,
sandbox: null,
scope: null,
scoped: HAS_BOOLEAN_VALUE,
scrolling: null,
seamless: MUST_USE_ATTRIBUTE | HAS_BOOLEAN_VALUE,
selected: MUST_USE_PROPERTY | HAS_BOOLEAN_VALUE,
@@ -3866,7 +3874,9 @@ var HTMLDOMPropertyConfig = {
itemID: MUST_USE_ATTRIBUTE,
itemRef: MUST_USE_ATTRIBUTE,
// property is supported for OpenGraph in meta tags.
property: null
property: null,
// IE-only attribute that controls focus behavior
unselectable: MUST_USE_ATTRIBUTE
},
DOMAttributeNames: {
acceptCharset: 'accept-charset',
@@ -4475,7 +4485,7 @@ if ("production" !== "development") {
if (typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ === 'undefined') {
console.debug(
'Download the React DevTools for a better development experience: ' +
'http://fb.me/react-devtools'
'https://fb.me/react-devtools'
);
}
}
@@ -4502,7 +4512,7 @@ if ("production" !== "development") {
if (!expectedFeatures[i]) {
console.error(
'One or more ES5 shim/shams expected by React are not available: ' +
'http://fb.me/react-warning-polyfills'
'https://fb.me/react-warning-polyfills'
);
break;
}
@@ -4510,7 +4520,7 @@ if ("production" !== "development") {
}
}
React.version = '0.13.1';
React.version = '0.13.3';
module.exports = React;
@@ -6229,7 +6239,7 @@ var ReactClass = {
("production" !== "development" ? warning(
this instanceof Constructor,
'Something is calling a React component directly. Use a factory or ' +
'JSX instead. See: http://fb.me/react-legacyfactory'
'JSX instead. See: https://fb.me/react-legacyfactory'
) : null);
}
@@ -6439,20 +6449,38 @@ ReactComponent.prototype.forceUpdate = function(callback) {
*/
if ("production" !== "development") {
var deprecatedAPIs = {
getDOMNode: 'getDOMNode',
isMounted: 'isMounted',
replaceProps: 'replaceProps',
replaceState: 'replaceState',
setProps: 'setProps'
getDOMNode: [
'getDOMNode',
'Use React.findDOMNode(component) instead.'
],
isMounted: [
'isMounted',
'Instead, make sure to clean up subscriptions and pending requests in ' +
'componentWillUnmount to prevent memory leaks.'
],
replaceProps: [
'replaceProps',
'Instead call React.render again at the top level.'
],
replaceState: [
'replaceState',
'Refactor your code to use setState instead (see ' +
'https://github.com/facebook/react/issues/3236).'
],
setProps: [
'setProps',
'Instead call React.render again at the top level.'
]
};
var defineDeprecationWarning = function(methodName, displayName) {
var defineDeprecationWarning = function(methodName, info) {
try {
Object.defineProperty(ReactComponent.prototype, methodName, {
get: function() {
("production" !== "development" ? warning(
false,
'%s(...) is deprecated in plain JavaScript React classes.',
displayName
'%s(...) is deprecated in plain JavaScript React classes. %s',
info[0],
info[1]
) : null);
return undefined;
}
@@ -6802,6 +6830,14 @@ var ReactCompositeComponentMixin = {
'Did you mean to define a state property instead?',
this.getName() || 'a component'
) : null);
("production" !== "development" ? warning(
!inst.getDefaultProps ||
inst.getDefaultProps.isReactClassApproved,
'getDefaultProps was defined on %s, a plain JavaScript class. ' +
'This is only supported for classes created using React.createClass. ' +
'Use a static property to define defaultProps instead.',
this.getName() || 'a component'
) : null);
("production" !== "development" ? warning(
!inst.propTypes,
'propTypes was defined as an instance property on %s. Use a static ' +
@@ -6838,6 +6874,7 @@ var ReactCompositeComponentMixin = {
this._pendingReplaceState = false;
this._pendingForceUpdate = false;
var childContext;
var renderedElement;
var previouslyMounting = ReactLifeCycle.currentlyMountingInstance;
@@ -6852,7 +6889,8 @@ var ReactCompositeComponentMixin = {
}
}
renderedElement = this._renderValidatedComponent();
childContext = this._getValidatedChildContext(context);
renderedElement = this._renderValidatedComponent(childContext);
} finally {
ReactLifeCycle.currentlyMountingInstance = previouslyMounting;
}
@@ -6866,7 +6904,7 @@ var ReactCompositeComponentMixin = {
this._renderedComponent,
rootID,
transaction,
this._processChildContext(context)
this._mergeChildContext(context, childContext)
);
if (inst.componentDidMount) {
transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);
@@ -6996,7 +7034,7 @@ var ReactCompositeComponentMixin = {
* @return {object}
* @private
*/
_processChildContext: function(currentContext) {
_getValidatedChildContext: function(currentContext) {
var inst = this._instance;
var childContext = inst.getChildContext && inst.getChildContext();
if (childContext) {
@@ -7021,6 +7059,13 @@ var ReactCompositeComponentMixin = {
name
) : invariant(name in inst.constructor.childContextTypes));
}
return childContext;
}
return null;
},
_mergeChildContext: function(currentContext, childContext) {
if (childContext) {
return assign({}, currentContext, childContext);
}
return currentContext;
@@ -7280,6 +7325,10 @@ var ReactCompositeComponentMixin = {
return inst.state;
}
if (replace && queue.length === 1) {
return queue[0];
}
var nextState = assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
@@ -7349,13 +7398,14 @@ var ReactCompositeComponentMixin = {
_updateRenderedComponent: function(transaction, context) {
var prevComponentInstance = this._renderedComponent;
var prevRenderedElement = prevComponentInstance._currentElement;
var nextRenderedElement = this._renderValidatedComponent();
var childContext = this._getValidatedChildContext();
var nextRenderedElement = this._renderValidatedComponent(childContext);
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
ReactReconciler.receiveComponent(
prevComponentInstance,
nextRenderedElement,
transaction,
this._processChildContext(context)
this._mergeChildContext(context, childContext)
);
} else {
// These two IDs are actually the same! But nothing should rely on that.
@@ -7371,7 +7421,7 @@ var ReactCompositeComponentMixin = {
this._renderedComponent,
thisID,
transaction,
context
this._mergeChildContext(context, childContext)
);
this._replaceNodeWithMarkupByID(prevComponentID, nextMarkup);
}
@@ -7409,11 +7459,12 @@ var ReactCompositeComponentMixin = {
/**
* @private
*/
_renderValidatedComponent: function() {
_renderValidatedComponent: function(childContext) {
var renderedComponent;
var previousContext = ReactContext.current;
ReactContext.current = this._processChildContext(
this._currentElement._context
ReactContext.current = this._mergeChildContext(
this._currentElement._context,
childContext
);
ReactCurrentOwner.current = this;
try {
@@ -7778,6 +7829,7 @@ var ReactDOM = mapObject({
// SVG
circle: 'circle',
clipPath: 'clipPath',
defs: 'defs',
ellipse: 'ellipse',
g: 'g',
@@ -7927,11 +7979,13 @@ function assertValidProps(props) {
'Can only set one of `children` or `props.dangerouslySetInnerHTML`.'
) : invariant(props.children == null));
("production" !== "development" ? invariant(
props.dangerouslySetInnerHTML.__html != null,
typeof props.dangerouslySetInnerHTML === 'object' &&
'__html' in props.dangerouslySetInnerHTML,
'`props.dangerouslySetInnerHTML` must be in the form `{__html: ...}`. ' +
'Please visit http://fb.me/react-invariant-dangerously-set-inner-html ' +
'Please visit https://fb.me/react-invariant-dangerously-set-inner-html ' +
'for more information.'
) : invariant(props.dangerouslySetInnerHTML.__html != null));
) : invariant(typeof props.dangerouslySetInnerHTML === 'object' &&
'__html' in props.dangerouslySetInnerHTML));
}
if ("production" !== "development") {
("production" !== "development" ? warning(
@@ -8239,6 +8293,8 @@ ReactDOMComponent.Mixin = {
if (propKey === STYLE) {
if (nextProp) {
nextProp = this._previousStyleCopy = assign({}, nextProp);
} else {
this._previousStyleCopy = null;
}
if (lastProp) {
// Unset styles on `lastProp` but not on `nextProp`.
@@ -10721,7 +10777,7 @@ function warnAndMonitorForKeyUse(message, element, parentType) {
("production" !== "development" ? warning(
false,
message + '%s%s See http://fb.me/react-warning-keys for more information.',
message + '%s%s See https://fb.me/react-warning-keys for more information.',
parentOrOwnerAddendum,
childOwnerAddendum
) : null);
@@ -10845,9 +10901,9 @@ function warnForPropsMutation(propName, element) {
("production" !== "development" ? warning(
false,
'Don\'t set .props.%s of the React component%s. ' +
'Instead, specify the correct value when ' +
'initially creating the element.%s',
'Don\'t set .props.%s of the React component%s. Instead, specify the ' +
'correct value when initially creating the element or use ' +
'React.cloneElement to make a new element with updated props.%s',
propName,
elementInfo,
ownerInfo
@@ -15236,6 +15292,7 @@ var ReactUpdates = _dereq_(100);
var SyntheticEvent = _dereq_(108);
var assign = _dereq_(29);
var emptyObject = _dereq_(130);
var topLevelTypes = EventConstants.topLevelTypes;
@@ -15577,6 +15634,9 @@ assign(
);
ReactShallowRenderer.prototype.render = function(element, context) {
if (!context) {
context = emptyObject;
}
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
this._render(element, transaction, context);
ReactUpdates.ReactReconcileTransaction.release(transaction);
@@ -15717,7 +15777,7 @@ for (eventType in topLevelTypes) {
module.exports = ReactTestUtils;
},{"100":100,"108":108,"16":16,"18":18,"21":21,"29":29,"31":31,"33":33,"43":43,"63":63,"65":65,"72":72,"73":73,"77":77}],96:[function(_dereq_,module,exports){
},{"100":100,"108":108,"130":130,"16":16,"18":18,"21":21,"29":29,"31":31,"33":33,"43":43,"63":63,"65":65,"72":72,"73":73,"77":77}],96:[function(_dereq_,module,exports){
/**
* Copyright 2013-2015, Facebook, Inc.
* All rights reserved.
@@ -16762,6 +16822,7 @@ var MUST_USE_ATTRIBUTE = DOMProperty.injection.MUST_USE_ATTRIBUTE;
var SVGDOMPropertyConfig = {
Properties: {
clipPath: MUST_USE_ATTRIBUTE,
cx: MUST_USE_ATTRIBUTE,
cy: MUST_USE_ATTRIBUTE,
d: MUST_USE_ATTRIBUTE,
@@ -16807,6 +16868,7 @@ var SVGDOMPropertyConfig = {
y: MUST_USE_ATTRIBUTE
},
DOMAttributeNames: {
clipPath: 'clip-path',
fillOpacity: 'fill-opacity',
fontFamily: 'font-family',
fontSize: 'font-size',
@@ -19713,6 +19775,7 @@ var shouldWrap = {
// Force wrapping for SVG elements because if they get created inside a <div>,
// they will be initialized in the wrong namespace (and will not display).
'circle': true,
'clipPath': true,
'defs': true,
'ellipse': true,
'g': true,
@@ -19755,6 +19818,7 @@ var markupWrap = {
'th': trWrap,
'circle': svgWrap,
'clipPath': svgWrap,
'defs': svgWrap,
'ellipse': svgWrap,
'g': svgWrap,
@@ -20100,6 +20164,7 @@ assign(
function isInternalComponentType(type) {
return (
typeof type === 'function' &&
typeof type.prototype !== 'undefined' &&
typeof type.prototype.mountComponent === 'function' &&
typeof type.prototype.receiveComponent === 'function'
);
@@ -21355,11 +21420,14 @@ module.exports = traverseAllChildren;
* @providesModule update
*/
/* global hasOwnProperty:true */
'use strict';
var assign = _dereq_(29);
var keyOf = _dereq_(157);
var invariant = _dereq_(150);
var hasOwnProperty = {}.hasOwnProperty;
function shallowCopy(x) {
if (Array.isArray(x)) {
@@ -21419,7 +21487,7 @@ function update(value, spec) {
COMMAND_SET
) : invariant(typeof spec === 'object'));
if (spec.hasOwnProperty(COMMAND_SET)) {
if (hasOwnProperty.call(spec, COMMAND_SET)) {
("production" !== "development" ? invariant(
Object.keys(spec).length === 1,
'Cannot have more than one key in an object with %s',
@@ -21431,7 +21499,7 @@ function update(value, spec) {
var nextValue = shallowCopy(value);
if (spec.hasOwnProperty(COMMAND_MERGE)) {
if (hasOwnProperty.call(spec, COMMAND_MERGE)) {
var mergeObj = spec[COMMAND_MERGE];
("production" !== "development" ? invariant(
mergeObj && typeof mergeObj === 'object',
@@ -21448,21 +21516,21 @@ function update(value, spec) {
assign(nextValue, spec[COMMAND_MERGE]);
}
if (spec.hasOwnProperty(COMMAND_PUSH)) {
if (hasOwnProperty.call(spec, COMMAND_PUSH)) {
invariantArrayCase(value, spec, COMMAND_PUSH);
spec[COMMAND_PUSH].forEach(function(item) {
nextValue.push(item);
});
}
if (spec.hasOwnProperty(COMMAND_UNSHIFT)) {
if (hasOwnProperty.call(spec, COMMAND_UNSHIFT)) {
invariantArrayCase(value, spec, COMMAND_UNSHIFT);
spec[COMMAND_UNSHIFT].forEach(function(item) {
nextValue.unshift(item);
});
}
if (spec.hasOwnProperty(COMMAND_SPLICE)) {
if (hasOwnProperty.call(spec, COMMAND_SPLICE)) {
("production" !== "development" ? invariant(
Array.isArray(value),
'Expected %s target to be an array; got %s',
@@ -21488,7 +21556,7 @@ function update(value, spec) {
});
}
if (spec.hasOwnProperty(COMMAND_APPLY)) {
if (hasOwnProperty.call(spec, COMMAND_APPLY)) {
("production" !== "development" ? invariant(
typeof spec[COMMAND_APPLY] === 'function',
'update(): expected spec of %s to be a function; got %s.',

18
web/static/js/react-with-addons-0.13.3.min.js поставляемый Обычный файл

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@@ -18,12 +18,13 @@
<link rel="manifest" href="/static/config/manifest.json">
<!-- Android add to homescreen -->
<link rel="stylesheet" href="/static/css/bootstrap-3.3.1.min.css">
<link rel="stylesheet" href="/static/css/bootstrap-3.3.5.min.css">
<link rel="stylesheet" href="/static/css/jasny-bootstrap.min.css" rel="stylesheet">
<script src="/static/js/react-with-addons-0.13.1.js"></script>
<script src="/static/js/react-with-addons-0.13.3.js"></script>
<script src="/static/js/jquery-1.11.1.js"></script>
<script src="/static/js/bootstrap-3.3.1.js"></script>
<script src="/static/js/bootstrap-3.3.5.js"></script>
<script src="/static/js/react-bootstrap-0.25.1.js"></script>
<link id="favicon" rel="icon" href="/static/images/favicon.ico" type="image/x-icon">
<link rel="shortcut icon" href="/static/images/favicon.ico" type="image/x-icon">