Merge branch 'master' into PLT-25
Этот коммит содержится в:
@@ -136,16 +136,15 @@ export default class ChannelNotifications extends React.Component {
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div>
|
||||
<div key='channel-notification-level-radio'>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
type='radio'
|
||||
checked={notifyActive[0]}
|
||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'default')}
|
||||
>
|
||||
/>
|
||||
{`Global default (${globalNotifyLevelName})`}
|
||||
</input>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
@@ -155,9 +154,8 @@ export default class ChannelNotifications extends React.Component {
|
||||
type='radio'
|
||||
checked={notifyActive[1]}
|
||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'all')}
|
||||
>
|
||||
/>
|
||||
{'For all activity'}
|
||||
</input>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
@@ -167,9 +165,8 @@ export default class ChannelNotifications extends React.Component {
|
||||
type='radio'
|
||||
checked={notifyActive[2]}
|
||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'mention')}
|
||||
>
|
||||
/>
|
||||
{'Only for mentions'}
|
||||
</input>
|
||||
</label>
|
||||
<br/>
|
||||
</div>
|
||||
@@ -179,9 +176,8 @@ export default class ChannelNotifications extends React.Component {
|
||||
type='radio'
|
||||
checked={notifyActive[3]}
|
||||
onChange={this.handleUpdateNotifyLevel.bind(this, 'none')}
|
||||
>
|
||||
/>
|
||||
{'Never'}
|
||||
</input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -274,16 +270,15 @@ export default class ChannelNotifications extends React.Component {
|
||||
|
||||
if (this.state.activeSection === 'markUnreadLevel') {
|
||||
const inputs = [(
|
||||
<div>
|
||||
<div key='channel-notification-unread-radio'>
|
||||
<div className='radio'>
|
||||
<label>
|
||||
<input
|
||||
type='radio'
|
||||
checked={this.state.markUnreadLevel === 'all'}
|
||||
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'all')}
|
||||
>
|
||||
/>
|
||||
{'For all unread messages'}
|
||||
</input>
|
||||
</label>
|
||||
<br />
|
||||
</div>
|
||||
@@ -293,9 +288,8 @@ export default class ChannelNotifications extends React.Component {
|
||||
type='radio'
|
||||
checked={this.state.markUnreadLevel === 'mention'}
|
||||
onChange={this.handleUpdateMarkUnreadLevel.bind(this, 'mention')}
|
||||
>
|
||||
/>
|
||||
{'Only for mentions'}
|
||||
</input>
|
||||
</label>
|
||||
<br />
|
||||
</div>
|
||||
@@ -370,7 +364,7 @@ export default class ChannelNotifications extends React.Component {
|
||||
data-dismiss='modal'
|
||||
>
|
||||
<span aria-hidden='true'>×</span>
|
||||
<span className='sr-only'>Close</span>
|
||||
<span className='sr-only'>{'Close'}</span>
|
||||
</button>
|
||||
<h4 className='modal-title'>Notification Preferences for <span className='name'>{this.state.title}</span></h4>
|
||||
</div>
|
||||
|
||||
@@ -260,6 +260,12 @@ export default class InviteMemberModal extends React.Component {
|
||||
|
||||
var content = null;
|
||||
var sendButton = null;
|
||||
|
||||
var sendButtonLabel = 'Send Invitation';
|
||||
if (this.state.inviteIds.length > 1) {
|
||||
sendButtonLabel = 'Send Invitations';
|
||||
}
|
||||
|
||||
if (this.state.emailEnabled) {
|
||||
content = (
|
||||
<div>
|
||||
@@ -281,7 +287,7 @@ export default class InviteMemberModal extends React.Component {
|
||||
onClick={this.handleSubmit}
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
>Send Invitations</button>
|
||||
>{sendButtonLabel}</button>
|
||||
);
|
||||
} else {
|
||||
var teamInviteLink = null;
|
||||
|
||||
@@ -169,7 +169,7 @@ export default class MoreDirectChannels extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<tr key={'direct-channel-row-user' + user.id}>
|
||||
<td
|
||||
key={user.id}
|
||||
className='direct-channel'
|
||||
|
||||
249
web/react/components/search_autocomplete.jsx
Обычный файл
249
web/react/components/search_autocomplete.jsx
Обычный файл
@@ -0,0 +1,249 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
const ChannelStore = require('../stores/channel_store.jsx');
|
||||
const KeyCodes = require('../utils/constants.jsx').KeyCodes;
|
||||
const UserStore = require('../stores/user_store.jsx');
|
||||
const Utils = require('../utils/utils.jsx');
|
||||
|
||||
const patterns = new Map([
|
||||
['channels', /\b(?:in|channel):\s*(\S*)$/i],
|
||||
['users', /\bfrom:\s*(\S*)$/i]
|
||||
]);
|
||||
|
||||
export default class SearchAutocomplete extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
this.handleDocumentClick = this.handleDocumentClick.bind(this);
|
||||
this.handleInputChange = this.handleInputChange.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
|
||||
this.completeWord = this.completeWord.bind(this);
|
||||
this.updateSuggestions = this.updateSuggestions.bind(this);
|
||||
|
||||
this.state = {
|
||||
show: false,
|
||||
mode: '',
|
||||
filter: '',
|
||||
selection: 0,
|
||||
suggestions: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
$(document).on('click', this.handleDocumentClick);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
$(document).off('click', this.handleDocumentClick);
|
||||
}
|
||||
|
||||
handleClick(value) {
|
||||
this.completeWord(value);
|
||||
}
|
||||
|
||||
handleDocumentClick(e) {
|
||||
const container = $(ReactDOM.findDOMNode(this.refs.container));
|
||||
|
||||
if (!(container.is(e.target) || container.has(e.target).length > 0)) {
|
||||
this.setState({
|
||||
show: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleInputChange(textbox, text) {
|
||||
const caret = Utils.getCaretPosition(textbox);
|
||||
const preText = text.substring(0, caret);
|
||||
|
||||
let mode = '';
|
||||
let filter = '';
|
||||
for (const [modeForPattern, pattern] of patterns) {
|
||||
const result = pattern.exec(preText);
|
||||
|
||||
if (result) {
|
||||
mode = modeForPattern;
|
||||
filter = result[1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (mode !== this.state.mode || filter !== this.state.filter) {
|
||||
this.updateSuggestions(mode, filter);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
mode,
|
||||
filter,
|
||||
show: mode || filter
|
||||
});
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (!this.state.show || this.state.suggestions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.which === KeyCodes.UP || e.which === KeyCodes.DOWN) {
|
||||
e.preventDefault();
|
||||
|
||||
let selection = this.state.selection;
|
||||
|
||||
if (e.which === KeyCodes.UP) {
|
||||
selection -= 1;
|
||||
} else {
|
||||
selection += 1;
|
||||
}
|
||||
|
||||
if (selection >= 0 && selection < this.state.suggestions.length) {
|
||||
this.setState({
|
||||
selection
|
||||
});
|
||||
}
|
||||
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) {
|
||||
e.preventDefault();
|
||||
|
||||
this.completeSelectedWord();
|
||||
}
|
||||
}
|
||||
|
||||
completeSelectedWord() {
|
||||
if (this.state.mode === 'channels') {
|
||||
this.completeWord(this.state.suggestions[this.state.selection].name);
|
||||
} else if (this.state.mode === 'users') {
|
||||
this.completeWord(this.state.suggestions[this.state.selection].username);
|
||||
}
|
||||
}
|
||||
|
||||
completeWord(value) {
|
||||
// add a space so that anything else typed doesn't interfere with the search flag
|
||||
this.props.completeWord(this.state.filter, value + ' ');
|
||||
|
||||
this.setState({
|
||||
show: false,
|
||||
mode: '',
|
||||
filter: '',
|
||||
selection: 0
|
||||
});
|
||||
}
|
||||
|
||||
updateSuggestions(mode, filter) {
|
||||
let suggestions = [];
|
||||
|
||||
if (mode === 'channels') {
|
||||
let channels = ChannelStore.getAll();
|
||||
|
||||
if (filter) {
|
||||
channels = channels.filter((channel) => channel.name.startsWith(filter));
|
||||
}
|
||||
|
||||
channels.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
suggestions = channels;
|
||||
} else if (mode === 'users') {
|
||||
let users = UserStore.getActiveOnlyProfileList();
|
||||
|
||||
if (filter) {
|
||||
users = users.filter((user) => user.username.startsWith(filter));
|
||||
}
|
||||
|
||||
users.sort((a, b) => a.username.localeCompare(b.username));
|
||||
|
||||
suggestions = users;
|
||||
}
|
||||
|
||||
let selection = this.state.selection;
|
||||
|
||||
// keep the same user/channel selected if it's still visible as a suggestion
|
||||
if (selection > 0 && this.state.suggestions.length > 0) {
|
||||
// we can't just use indexOf to find if the selection is still in the list since they are different javascript objects
|
||||
const currentSelectionId = this.state.suggestions[selection].id;
|
||||
let found = false;
|
||||
|
||||
for (let i = 0; i < suggestions.length; i++) {
|
||||
if (suggestions[i].id === currentSelectionId) {
|
||||
selection = i;
|
||||
found = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
selection = 0;
|
||||
}
|
||||
} else {
|
||||
selection = 0;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
suggestions,
|
||||
selection
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.state.show || this.state.suggestions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let suggestions = [];
|
||||
|
||||
if (this.state.mode === 'channels') {
|
||||
suggestions = this.state.suggestions.map((channel, index) => {
|
||||
let className = 'search-autocomplete__channel';
|
||||
if (this.state.selection === index) {
|
||||
className += ' selected';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={channel.name}
|
||||
ref={channel.name}
|
||||
onClick={this.handleClick.bind(this, channel.name)}
|
||||
className={className}
|
||||
>
|
||||
{channel.name}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
} else if (this.state.mode === 'users') {
|
||||
suggestions = this.state.suggestions.map((user, index) => {
|
||||
let className = 'search-autocomplete__user';
|
||||
if (this.state.selection === index) {
|
||||
className += ' selected';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
key={user.username}
|
||||
ref={user.username}
|
||||
onClick={this.handleClick.bind(this, user.username)}
|
||||
className={className}
|
||||
>
|
||||
<img
|
||||
className='profile-img'
|
||||
src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
|
||||
/>
|
||||
{user.username}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref='container'
|
||||
className='search-autocomplete'
|
||||
>
|
||||
{suggestions}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchAutocomplete.propTypes = {
|
||||
completeWord: React.PropTypes.func.isRequired
|
||||
};
|
||||
@@ -9,6 +9,7 @@ var utils = require('../utils/utils.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
var Popover = ReactBootstrap.Popover;
|
||||
var SearchAutocomplete = require('./search_autocomplete.jsx');
|
||||
|
||||
export default class SearchBar extends React.Component {
|
||||
constructor() {
|
||||
@@ -16,11 +17,13 @@ export default class SearchBar extends React.Component {
|
||||
this.mounted = false;
|
||||
|
||||
this.onListenerChange = this.onListenerChange.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleUserInput = this.handleUserInput.bind(this);
|
||||
this.handleUserFocus = this.handleUserFocus.bind(this);
|
||||
this.handleUserBlur = this.handleUserBlur.bind(this);
|
||||
this.performSearch = this.performSearch.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.completeWord = this.completeWord.bind(this);
|
||||
|
||||
const state = this.getSearchTermStateFromStores();
|
||||
state.focused = false;
|
||||
@@ -74,11 +77,18 @@ export default class SearchBar extends React.Component {
|
||||
results: null
|
||||
});
|
||||
}
|
||||
handleKeyDown(e) {
|
||||
if (this.refs.autocomplete) {
|
||||
this.refs.autocomplete.handleKeyDown(e);
|
||||
}
|
||||
}
|
||||
handleUserInput(e) {
|
||||
var term = e.target.value;
|
||||
PostStore.storeSearchTerm(term);
|
||||
PostStore.emitSearchTermChange(false);
|
||||
this.setState({searchTerm: term});
|
||||
|
||||
this.refs.autocomplete.handleInputChange(e.target, term);
|
||||
}
|
||||
handleMouseInput(e) {
|
||||
e.preventDefault();
|
||||
@@ -97,7 +107,7 @@ export default class SearchBar extends React.Component {
|
||||
this.setState({isSearching: true});
|
||||
client.search(
|
||||
terms,
|
||||
function success(data) {
|
||||
(data) => {
|
||||
this.setState({isSearching: false});
|
||||
if (utils.isMobile()) {
|
||||
ReactDOM.findDOMNode(this.refs.search).value = '';
|
||||
@@ -108,11 +118,11 @@ export default class SearchBar extends React.Component {
|
||||
results: data,
|
||||
is_mention_search: isMentionSearch
|
||||
});
|
||||
}.bind(this),
|
||||
function error(err) {
|
||||
},
|
||||
(err) => {
|
||||
this.setState({isSearching: false});
|
||||
AsyncClient.dispatchError(err, 'search');
|
||||
}.bind(this)
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -120,6 +130,24 @@ export default class SearchBar extends React.Component {
|
||||
e.preventDefault();
|
||||
this.performSearch(this.state.searchTerm.trim());
|
||||
}
|
||||
|
||||
completeWord(partialWord, word) {
|
||||
const textbox = ReactDOM.findDOMNode(this.refs.search);
|
||||
let text = textbox.value;
|
||||
|
||||
const caret = utils.getCaretPosition(textbox);
|
||||
const preText = text.substring(0, caret - partialWord.length);
|
||||
const postText = text.substring(caret);
|
||||
text = preText + word + postText;
|
||||
|
||||
textbox.value = text;
|
||||
utils.setCaretPosition(textbox, preText.length + word.length);
|
||||
|
||||
PostStore.storeSearchTerm(text);
|
||||
PostStore.emitSearchTermChange(false);
|
||||
this.setState({searchTerm: text});
|
||||
}
|
||||
|
||||
render() {
|
||||
var isSearching = null;
|
||||
if (this.state.isSearching) {
|
||||
@@ -143,12 +171,13 @@ export default class SearchBar extends React.Component {
|
||||
className='search__clear'
|
||||
onClick={this.clearFocus}
|
||||
>
|
||||
Cancel
|
||||
{'Cancel'}
|
||||
</span>
|
||||
<form
|
||||
role='form'
|
||||
className='search__form relative-div'
|
||||
onSubmit={this.handleSubmit}
|
||||
style={{overflow: 'visible'}}
|
||||
>
|
||||
<span className='glyphicon glyphicon-search sidebar__search-icon' />
|
||||
<input
|
||||
@@ -160,10 +189,16 @@ export default class SearchBar extends React.Component {
|
||||
onFocus={this.handleUserFocus}
|
||||
onBlur={this.handleUserBlur}
|
||||
onChange={this.handleUserInput}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
onMouseUp={this.handleMouseInput}
|
||||
/>
|
||||
{isSearching}
|
||||
<SearchAutocomplete
|
||||
ref='autocomplete'
|
||||
completeWord={this.completeWord}
|
||||
/>
|
||||
<Popover
|
||||
id='searchbar-help-popup'
|
||||
placement='bottom'
|
||||
className={helpClass}
|
||||
>
|
||||
|
||||
@@ -36,7 +36,7 @@ export default class SettingItemMax extends React.Component {
|
||||
if (this.props.width === 'full') {
|
||||
widthClass = 'col-sm-12';
|
||||
} else {
|
||||
widthClass = 'col-sm-9 col-sm-offset-3';
|
||||
widthClass = 'col-sm-10 col-sm-offset-2';
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
export default class SettingsSidebar extends React.Component {
|
||||
componentDidUpdate() {
|
||||
$('.settings-modal').find('.modal-body').scrollTop(0);
|
||||
$('.settings-modal').find('.modal-body').perfectScrollbar('update');
|
||||
}
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export default class TeamSettingsModal extends React.Component {
|
||||
componentDidMount() {
|
||||
$('body').on('click', '.modal-back', function handleBackClick() {
|
||||
$(this).closest('.modal-dialog').removeClass('display--content');
|
||||
$(this).closest('.modal-dialog').find('.settings-table .nav li.active').removeClass('active');
|
||||
});
|
||||
$('body').on('click', '.modal-header .close', () => {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -15,11 +15,6 @@ export default class TeamSignupSendInvitesPage extends React.Component {
|
||||
this.state = {
|
||||
emailEnabled: global.window.mm_config.SendEmailNotifications === 'true'
|
||||
};
|
||||
|
||||
if (!this.state.emailEnabled) {
|
||||
this.props.state.wizard = 'username';
|
||||
this.props.updateParent(this.props.state);
|
||||
}
|
||||
}
|
||||
submitBack(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -54,7 +54,11 @@ export default class TeamSignupUrlPage extends React.Component {
|
||||
if (data) {
|
||||
this.setState({nameError: 'This URL is unavailable. Please try another.'});
|
||||
} else {
|
||||
this.props.state.wizard = 'send_invites';
|
||||
if (global.window.mm_config.SendEmailNotifications === 'true') {
|
||||
this.props.state.wizard = 'send_invites';
|
||||
} else {
|
||||
this.props.state.wizard = 'username';
|
||||
}
|
||||
this.props.state.team.type = 'O';
|
||||
|
||||
this.props.state.team.name = name;
|
||||
|
||||
@@ -104,21 +104,19 @@ export default class TeamSignupWelcomePage extends React.Component {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
<img
|
||||
className='signup-team-logo'
|
||||
src='/static/images/logo.png'
|
||||
/>
|
||||
<h3 className='sub-heading'>Welcome to:</h3>
|
||||
<h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1>
|
||||
</p>
|
||||
<img
|
||||
className='signup-team-logo'
|
||||
src='/static/images/logo.png'
|
||||
/>
|
||||
<h3 className='sub-heading'>Welcome to:</h3>
|
||||
<h1 className='margin--top-none'>{global.window.mm_config.SiteName}</h1>
|
||||
<p className='margin--less'>Let's set up your new team</p>
|
||||
<p>
|
||||
<div>
|
||||
Please confirm your email address:<br />
|
||||
<div className='inner__content'>
|
||||
<div className='block--gray'>{this.props.state.team.email}</div>
|
||||
</div>
|
||||
</p>
|
||||
</div>
|
||||
<p className='margin--extra color--light'>
|
||||
Your account will administer the new team site. <br />
|
||||
You can add other administrators later.
|
||||
|
||||
@@ -119,24 +119,23 @@ export default class ManageIncomingHooks extends React.Component {
|
||||
hooks.push(
|
||||
<div
|
||||
key={hook.id}
|
||||
className='font--small'
|
||||
className='webhook__item'
|
||||
>
|
||||
<div className='padding-top x2 divider-light'></div>
|
||||
<div className='padding-top x2'>
|
||||
<strong>{'URL: '}</strong><span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
|
||||
<div className='padding-top x2 webhook__url'>
|
||||
<strong>{'URL: '}</strong>
|
||||
<span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
|
||||
</div>
|
||||
<div className='padding-top'>
|
||||
<strong>{'Channel: '}</strong>{c.display_name}
|
||||
</div>
|
||||
<div className='padding-top'>
|
||||
<a
|
||||
className={'text-danger'}
|
||||
href='#'
|
||||
onClick={this.removeHook.bind(this, hook.id)}
|
||||
>
|
||||
{'Remove'}
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
className={'webhook__remove'}
|
||||
href='#'
|
||||
onClick={this.removeHook.bind(this, hook.id)}
|
||||
>
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
</a>
|
||||
<div className='padding-top x2 divider-light'></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -148,35 +147,38 @@ export default class ManageIncomingHooks extends React.Component {
|
||||
} else if (hooks.length > 0) {
|
||||
displayHooks = hooks;
|
||||
} else {
|
||||
displayHooks = <label>{': None'}</label>;
|
||||
displayHooks = <div className='padding-top x2'>{'None'}</div>;
|
||||
}
|
||||
|
||||
const existingHooks = (
|
||||
<div className='padding-top x2'>
|
||||
<div className='webhooks__container'>
|
||||
<label className='control-label padding-top x2'>{'Existing incoming webhooks'}</label>
|
||||
{displayHooks}
|
||||
<div className='padding-top divider-light'></div>
|
||||
<div className='webhooks__list'>
|
||||
{displayHooks}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key='addIncomingHook'>
|
||||
{'Create webhook URLs for use in external integrations. Please see '}<a href='http://mattermost.org/webhooks'>{'http://mattermost.org/webhooks'}</a> {' to learn more.'}
|
||||
<br/>
|
||||
<br/>
|
||||
<label className='control-label'>{'Add a new incoming webhook'}</label>
|
||||
<div className='padding-top'>
|
||||
<select
|
||||
ref='channelName'
|
||||
className='form-control'
|
||||
value={this.state.channelId}
|
||||
onChange={this.updateChannelId}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
{serverError}
|
||||
<div className='padding-top'>
|
||||
<label className='control-label padding-top x2'>{'Add a new incoming webhook'}</label>
|
||||
<div className='row padding-top'>
|
||||
<div className='col-sm-10 padding-bottom'>
|
||||
<select
|
||||
ref='channelName'
|
||||
className='form-control'
|
||||
value={this.state.channelId}
|
||||
onChange={this.updateChannelId}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
{serverError}
|
||||
</div>
|
||||
<div className='col-sm-2 col-xs-4 no-padding--left padding-bottom'>
|
||||
<a
|
||||
className={'btn btn-sm btn-primary' + disableButton}
|
||||
className={'btn form-control no-padding btn-sm btn-primary' + disableButton}
|
||||
href='#'
|
||||
onClick={this.addNewHook}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ var Constants = require('../../utils/constants.jsx');
|
||||
var ChannelStore = require('../../stores/channel_store.jsx');
|
||||
var LoadingScreen = require('../loading_screen.jsx');
|
||||
|
||||
|
||||
export default class ManageOutgoingHooks extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
@@ -180,9 +181,8 @@ export default class ManageOutgoingHooks extends React.Component {
|
||||
hooks.push(
|
||||
<div
|
||||
key={hook.id}
|
||||
className='font--small'
|
||||
className='webhook__item'
|
||||
>
|
||||
<div className='padding-top x2 divider-light'></div>
|
||||
<div className='padding-top x2'>
|
||||
<strong>{'URLs: '}</strong><span className='word-break--all'>{hook.callback_urls.join(', ')}</span>
|
||||
</div>
|
||||
@@ -199,15 +199,15 @@ export default class ManageOutgoingHooks extends React.Component {
|
||||
>
|
||||
{'Regen Token'}
|
||||
</a>
|
||||
<span>{' - '}</span>
|
||||
<a
|
||||
className='text-danger'
|
||||
className='webhook__remove'
|
||||
href='#'
|
||||
onClick={this.removeHook.bind(this, hook.id)}
|
||||
>
|
||||
{'Remove'}
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className='padding-top x2 divider-light'></div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -218,13 +218,16 @@ export default class ManageOutgoingHooks extends React.Component {
|
||||
} else if (hooks.length > 0) {
|
||||
displayHooks = hooks;
|
||||
} else {
|
||||
displayHooks = <label>{': None'}</label>;
|
||||
displayHooks = <div className='padding-top x2'>{'None'}</div>;
|
||||
}
|
||||
|
||||
const existingHooks = (
|
||||
<div className='padding-top x2'>
|
||||
<div className='webhooks__container'>
|
||||
<label className='control-label padding-top x2'>{'Existing outgoing webhooks'}</label>
|
||||
{displayHooks}
|
||||
<div className='padding-top divider-light'></div>
|
||||
<div className='webhooks__list'>
|
||||
{displayHooks}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -234,41 +237,49 @@ export default class ManageOutgoingHooks extends React.Component {
|
||||
<div key='addOutgoingHook'>
|
||||
<label className='control-label'>{'Add a new outgoing webhook'}</label>
|
||||
<div className='padding-top'>
|
||||
<strong>{'Channel:'}</strong>
|
||||
<select
|
||||
ref='channelName'
|
||||
className='form-control'
|
||||
value={this.state.channelId}
|
||||
onChange={this.updateChannelId}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
<span>{'Only public channels can be used'}</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<strong>{'Trigger Words:'}</strong>
|
||||
<input
|
||||
ref='triggerWords'
|
||||
className='form-control'
|
||||
value={this.state.triggerWords}
|
||||
onChange={this.updateTriggerWords}
|
||||
placeholder='Optional if channel selected'
|
||||
/>
|
||||
<span>{'Comma separated words to trigger on'}</span>
|
||||
<br/>
|
||||
<br/>
|
||||
<strong>{'Callback URLs:'}</strong>
|
||||
<textarea
|
||||
ref='callbackURLs'
|
||||
className='form-control no-resize'
|
||||
value={this.state.callbackURLs}
|
||||
resize={false}
|
||||
rows={3}
|
||||
onChange={this.updateCallbackURLs}
|
||||
/>
|
||||
<span>{'New line separated URLs that will receive the HTTP POST event'}</span>
|
||||
{serverError}
|
||||
<div className='padding-top'>
|
||||
<div>
|
||||
<label className='control-label'>{'Channel'}</label>
|
||||
<div className='padding-top'>
|
||||
<select
|
||||
ref='channelName'
|
||||
className='form-control'
|
||||
value={this.state.channelId}
|
||||
onChange={this.updateChannelId}
|
||||
>
|
||||
{options}
|
||||
</select>
|
||||
</div>
|
||||
<div className='padding-top'>{'Only public channels can be used'}</div>
|
||||
</div>
|
||||
<div className='padding-top x2'>
|
||||
<label className='control-label'>{'Trigger Words:'}</label>
|
||||
<div className='padding-top'>
|
||||
<input
|
||||
ref='triggerWords'
|
||||
className='form-control'
|
||||
value={this.state.triggerWords}
|
||||
onChange={this.updateTriggerWords}
|
||||
placeholder='Optional if channel selected'
|
||||
/>
|
||||
</div>
|
||||
<div className='padding-top'>{'Comma separated words to trigger on'}</div>
|
||||
</div>
|
||||
<div className='padding-top x2'>
|
||||
<label className='control-label'>{'Callback URLs:'}</label>
|
||||
<div className='padding-top'>
|
||||
<textarea
|
||||
ref='callbackURLs'
|
||||
className='form-control no-resize'
|
||||
value={this.state.callbackURLs}
|
||||
resize={false}
|
||||
rows={3}
|
||||
onChange={this.updateCallbackURLs}
|
||||
/>
|
||||
</div>
|
||||
<div className='padding-top'>{'New line separated URLs that will receive the HTTP POST event'}</div>
|
||||
{serverError}
|
||||
</div>
|
||||
<div className='padding-top padding-bottom'>
|
||||
<a
|
||||
className={'btn btn-sm btn-primary'}
|
||||
href='#'
|
||||
|
||||
@@ -43,7 +43,6 @@ export default class UserSettingsIntegrationsTab extends React.Component {
|
||||
incomingHooksSection = (
|
||||
<SettingItemMax
|
||||
title='Incoming Webhooks'
|
||||
width = 'full'
|
||||
inputs={inputs}
|
||||
updateSection={(e) => {
|
||||
this.updateSection('');
|
||||
@@ -55,7 +54,6 @@ export default class UserSettingsIntegrationsTab extends React.Component {
|
||||
incomingHooksSection = (
|
||||
<SettingItemMin
|
||||
title='Incoming Webhooks'
|
||||
width = 'full'
|
||||
describe='Manage your incoming webhooks (Developer feature)'
|
||||
updateSection={() => {
|
||||
this.updateSection('incoming-hooks');
|
||||
|
||||
Ссылка в новой задаче
Block a user