mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
Merge branch 'master' into PLT-25
This commit is contained in:
5
CONTRIBUTING.md
Normal file
5
CONTRIBUTING.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Contributing
|
||||
|
||||
## Contributing Code
|
||||
|
||||
Please see [Mattermost Code Contribution Guidelines](https://github.com/mattermost/platform/blob/master/doc/developer/Code-Contribution-Guidelines.md)
|
||||
@@ -18,7 +18,7 @@
|
||||
<tr>
|
||||
<td style="border-bottom: 1px solid #ddd; padding: 0 0 20px;">
|
||||
<h2 style="font-weight: normal; margin-top: 10px;">You updated your password</h2>
|
||||
<p>You updated your password for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} by {{.Props.Method}}.<br> If this change wasn't initiated by you, please reply to this email and let us know.</p>
|
||||
<p>You updated your password for {{.Props.TeamDisplayName}} on {{ .Props.TeamURL }} by {{.Props.Method}}.<br>If this change wasn't initiated by you, please contact your system administrator.</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -8,4 +8,8 @@ Some things to know about search:
|
||||
- You can use quotes to return search results for exact terms, like `"Mattermost website"` will only return messages containing the entire phrase `"Mattermost website"` and not return messages with only `Mattermost` or `website`
|
||||
- You can use the `*` character for wildcard searches that match within words. For example: Searching for `rea*` brings back messages containing `reach`, `reason` and other words starting with `rea`.
|
||||
|
||||
Search in Mattermost uses the full text search features in MySQL and Postgres databases. Special cases that are not supported in default full text search, such as searching for IP addresses like `10.100.200.101`, can be added in future as the search feature evolves.
|
||||
#### Limitations
|
||||
|
||||
- Search in Mattermost uses the full text search features included in either a MySQL or Postgres database, which has some limitations
|
||||
- Special cases that are not supported in default full text search, such as searching for IP addresses like `10.100.200.101`, can be added in future as the search feature evolves
|
||||
- Searches with fewer than three characters will return no results, so for searching in Chinese try adding * to the end of queries
|
||||
|
||||
@@ -12,7 +12,8 @@ To enable email, configure an SMTP email service as follows:
|
||||
2. If you don't have an SMTP service, here are simple instructions to set one up with [Amazon Simple Email Service (SES)](https://aws.amazon.com/ses/):
|
||||
2. Go to [Amazon SES console](https://console.aws.amazon.com/ses) then `SMTP Settings > Create My SMTP Credentials`
|
||||
3. Copy the `Server Name`, `Port`, `SMTP Username`, and `SMTP Password` for Step 2 below.
|
||||
4. From the `Domains` menu set up and verify a new domain, then enable `Generate DKIM Settings` for the domain.
|
||||
4. From the `Domains` menu set up and verify a new domain, then enable `Generate DKIM Settings` for the domain.
|
||||
1. We recommend you set up _[Sender Policy Framework](https://en.wikipedia.org/wiki/Sender_Policy_Framework) (SPF)_ and/or _[Domain Keys Identified Mail](https://en.wikipedia.org/wiki/DomainKeys_Identified_Mail) (DKIM)_ for your email domain.
|
||||
5. Choose an sender address like `mattermost@example.com` and click `Send a Test Email` to verify setup is working correctly.
|
||||
|
||||
2. **Configure SMTP settings**
|
||||
@@ -57,7 +58,11 @@ To enable email, configure an SMTP email service as follows:
|
||||
* Information needed
|
||||
|
||||
##### Hotmail
|
||||
* Information needed
|
||||
* Set **SMTP Username** to **your_email@hotmail.com**
|
||||
* Set **SMTP Password** to **your_password**
|
||||
* Set **SMTP Server** to **smtp-mail.outlook.com**
|
||||
* Set **SMTP Port** to **587**
|
||||
* Set **Connection Security** to **STARTTLS**
|
||||
|
||||
|
||||
### Troubleshooting SMTP
|
||||
@@ -91,4 +96,4 @@ Connected to mail.example.com.
|
||||
250-STARTTLS
|
||||
250-PIPELINING
|
||||
250 8BITMIME
|
||||
```
|
||||
```
|
||||
|
||||
@@ -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
Normal file
249
web/react/components/search_autocomplete.jsx
Normal file
@@ -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');
|
||||
|
||||
@@ -74,9 +74,9 @@ class BrowserStoreClass {
|
||||
var result = null;
|
||||
try {
|
||||
if (this.isLocalStorageSupported()) {
|
||||
result = JSON.parse(getPrefix() + localStorage.getItem(name));
|
||||
result = JSON.parse(localStorage.getItem(getPrefix() + name));
|
||||
} else {
|
||||
result = JSON.parse(getPrefix() + sessionStorage.getItem(name));
|
||||
result = JSON.parse(sessionStorage.getItem(getPrefix() + name));
|
||||
}
|
||||
} catch (err) {
|
||||
result = null;
|
||||
|
||||
@@ -158,7 +158,7 @@ function handleNewPostEvent(msg) {
|
||||
// Update channel state
|
||||
if (ChannelStore.getCurrentId() === msg.channel_id) {
|
||||
if (window.isActive) {
|
||||
AsyncClient.updateLastViewedAt();
|
||||
AsyncClient.updateLastViewedAt(true);
|
||||
}
|
||||
} else {
|
||||
AsyncClient.getChannel(msg.channel_id);
|
||||
|
||||
@@ -48,6 +48,7 @@ class UserStoreClass extends EventEmitter {
|
||||
this.getProfilesUsernameMap = this.getProfilesUsernameMap.bind(this);
|
||||
this.getProfiles = this.getProfiles.bind(this);
|
||||
this.getActiveOnlyProfiles = this.getActiveOnlyProfiles.bind(this);
|
||||
this.getActiveOnlyProfileList = this.getActiveOnlyProfileList.bind(this);
|
||||
this.saveProfile = this.saveProfile.bind(this);
|
||||
this.setSessions = this.setSessions.bind(this);
|
||||
this.getSessions = this.getSessions.bind(this);
|
||||
@@ -215,6 +216,19 @@ class UserStoreClass extends EventEmitter {
|
||||
return active;
|
||||
}
|
||||
|
||||
getActiveOnlyProfileList() {
|
||||
const profileMap = this.getActiveOnlyProfiles();
|
||||
const profiles = [];
|
||||
|
||||
for (const id in profileMap) {
|
||||
if (profileMap.hasOwnProperty(id)) {
|
||||
profiles.push(profileMap[id]);
|
||||
}
|
||||
}
|
||||
|
||||
return profiles;
|
||||
}
|
||||
|
||||
saveProfile(profile) {
|
||||
var ps = this.getProfiles();
|
||||
ps[profile.id] = profile;
|
||||
|
||||
@@ -152,14 +152,14 @@ export function getChannel(id) {
|
||||
);
|
||||
}
|
||||
|
||||
export function updateLastViewedAt() {
|
||||
export function updateLastViewedAt(force) {
|
||||
const channelId = ChannelStore.getCurrentId();
|
||||
|
||||
if (channelId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isCallInProgress(`updateLastViewed${channelId}`)) {
|
||||
if (isCallInProgress(`updateLastViewed${channelId}`) && !force) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -311,6 +311,7 @@ module.exports = {
|
||||
RIGHT: 39,
|
||||
BACKSPACE: 8,
|
||||
ENTER: 13,
|
||||
ESCAPE: 27
|
||||
ESCAPE: 27,
|
||||
SPACE: 32
|
||||
}
|
||||
};
|
||||
|
||||
@@ -431,6 +431,7 @@ export function applyTheme(theme) {
|
||||
|
||||
if (theme.sidebarTextActiveColor) {
|
||||
changeCss('.sidebar--left .nav-pills__container li.active a, .sidebar--left .nav-pills__container li.active a:hover, .sidebar--left .nav-pills__container li.active a:focus, .settings-modal .nav-pills>li.active a, .settings-modal .nav-pills>li.active a:hover, .settings-modal .nav-pills>li.active a:active', 'color:' + theme.sidebarTextActiveColor, 2);
|
||||
changeCss('.sidebar--left .nav li.active a, .sidebar--left .nav li.active a:hover, .sidebar--left .nav li.active a:focus', 'background:' + changeOpacity(theme.sidebarTextActiveColor, 0.1), 1);
|
||||
}
|
||||
|
||||
if (theme.sidebarHeaderBg) {
|
||||
@@ -494,7 +495,7 @@ export function applyTheme(theme) {
|
||||
changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
|
||||
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
|
||||
changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
|
||||
changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
|
||||
changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td, .command-box, .modal .modal-content, .settings-modal .settings-table .settings-content .divider-light, .webhooks__container, .dropdown-menu, .modal .modal-header, .popover, .mentions--top .mentions-box', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
|
||||
changeCss('.popover.bottom>.arrow', 'border-bottom-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
|
||||
changeCss('.popover.right>.arrow', 'border-right-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
|
||||
changeCss('.popover.left>.arrow', 'border-left-color:' + changeOpacity(theme.centerChannelColor, 0.25), 1);
|
||||
@@ -509,7 +510,7 @@ export function applyTheme(theme) {
|
||||
changeCss('@media(max-width: 768px){.search-bar__container .search__form .search-bar', 'background:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: inherit;', 1);
|
||||
changeCss('.input-group-addon, .search-bar__container .search__form, .form-control', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
|
||||
changeCss('.form-control:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
|
||||
changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
|
||||
changeCss('.channel-intro .channel-intro__content, .webhooks__container', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
|
||||
changeCss('.date-separator .separator__text', 'color:' + theme.centerChannelColor, 2);
|
||||
changeCss('.date-separator .separator__hr, .modal-footer, .modal .custom-textarea, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
|
||||
changeCss('.modal .custom-textarea:focus', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.3), 1);
|
||||
@@ -521,7 +522,7 @@ export function applyTheme(theme) {
|
||||
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
|
||||
changeCss('.post:hover, .modal .more-table tbody>tr:hover td, .sidebar--right .sidebar--right__header, .settings-modal .settings-table .settings-content .section-min:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
|
||||
changeCss('.date-separator.hovered--before:after, .date-separator.hovered--after:before, .new-separator.hovered--after:before, .new-separator.hovered--before:after', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
|
||||
changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
|
||||
changeCss('.command-name:hover, .mentions-name:hover, .mentions-focus, .dropdown-menu>li>a:focus, .dropdown-menu>li>a:hover, .bot-indicator', 'background:' + changeOpacity(theme.centerChannelColor, 0.15), 1);
|
||||
changeCss('code', 'background:' + changeOpacity(theme.centerChannelColor, 0.1), 1);
|
||||
changeCss('.post.current--user:hover .post-body ', 'background: none;', 1);
|
||||
changeCss('.sidebar--right', 'color:' + theme.centerChannelColor, 2);
|
||||
|
||||
@@ -94,8 +94,11 @@ a:focus, a:hover {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
.text-danger, a.text-danger {
|
||||
color: #E05F5D;
|
||||
&:hover, &:focus {
|
||||
color: #E05F5D;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
@@ -112,6 +115,10 @@ a:focus, a:hover {
|
||||
&:focus {
|
||||
@include box-shadow(none);
|
||||
}
|
||||
&.no-padding {
|
||||
line-height: 32px;
|
||||
padding: 0;
|
||||
}
|
||||
&.no-resize {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
@@ -569,9 +569,10 @@ body.ios {
|
||||
}
|
||||
|
||||
.bot-indicator {
|
||||
background-color: lightgrey;
|
||||
border-radius:2px;
|
||||
padding-left:2px;
|
||||
padding-right:2px;
|
||||
font-family:"Courier New"
|
||||
font-family: inherit;
|
||||
font-size: 11px;
|
||||
padding: 2px 4px;
|
||||
border-radius: 2px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 0 -4px;
|
||||
}
|
||||
|
||||
@@ -284,6 +284,9 @@
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
.search-help-popover.visible {
|
||||
visibility: hidden;
|
||||
}
|
||||
.modal-direct-channels {
|
||||
.member-count {
|
||||
float: none;
|
||||
@@ -401,6 +404,9 @@
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
.no-padding--left {
|
||||
padding-left: 15px;
|
||||
}
|
||||
}
|
||||
.settings-links {
|
||||
display: none;
|
||||
@@ -425,6 +431,11 @@
|
||||
}
|
||||
}
|
||||
.settings-table {
|
||||
.nav {
|
||||
position: relative;
|
||||
top: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.settings-content {
|
||||
&.minimize-settings {
|
||||
padding: 0;
|
||||
|
||||
@@ -109,3 +109,43 @@
|
||||
.search-highlight {
|
||||
background-color: #FFF2BB;
|
||||
}
|
||||
|
||||
.search-autocomplete {
|
||||
background-color: #fff;
|
||||
border: $border-gray;
|
||||
line-height: 36px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
@extend %popover-box-shadow;
|
||||
}
|
||||
|
||||
.search-autocomplete__channel {
|
||||
cursor: pointer;
|
||||
height: 36px;
|
||||
padding: 0px 6px;
|
||||
|
||||
&.selected {
|
||||
background-color:rgba(51, 51, 51, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.search-autocomplete__user {
|
||||
cursor: pointer;
|
||||
height: 36px;
|
||||
padding: 0px;
|
||||
|
||||
.profile-img {
|
||||
height: 32px;
|
||||
margin-right: 6px;
|
||||
width: 32px;
|
||||
@include border-radius(16px);
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color:rgba(51, 51, 51, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "access-history";
|
||||
@import "activity-log";
|
||||
@import "webhooks";
|
||||
|
||||
.user-settings {
|
||||
min-height:300px;
|
||||
@@ -29,6 +30,9 @@
|
||||
li {
|
||||
list-style: none;
|
||||
}
|
||||
label {
|
||||
font-weight: 600;
|
||||
}
|
||||
.settings-table {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
@@ -37,6 +41,11 @@
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
}
|
||||
.nav {
|
||||
position: fixed;
|
||||
top: 57px;
|
||||
width: 180px;
|
||||
}
|
||||
.security-links {
|
||||
margin-right: 20px;
|
||||
.fa {
|
||||
@@ -129,10 +138,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.font--small {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.section-describe {
|
||||
@include opacity(0.7);
|
||||
white-space:pre;
|
||||
@@ -161,14 +166,29 @@
|
||||
|
||||
.setting-list-item {
|
||||
margin-top:7px;
|
||||
.has-error {
|
||||
color: #a94442;
|
||||
}
|
||||
.has-error {
|
||||
color: #a94442;
|
||||
}
|
||||
.no-padding--left {
|
||||
padding-left: 0;
|
||||
}
|
||||
.padding-top {
|
||||
padding-top: 7px;
|
||||
&.x2 {
|
||||
padding-top: 14px;
|
||||
}
|
||||
.padding-top {
|
||||
padding-top: 7px;
|
||||
&.x2 {
|
||||
padding-top: 14px;
|
||||
}
|
||||
&.x3 {
|
||||
padding-top: 21px;
|
||||
}
|
||||
}
|
||||
.padding-bottom {
|
||||
padding-bottom: 7px;
|
||||
&.x2 {
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
&.x3 {
|
||||
padding-bottom: 21px;
|
||||
}
|
||||
.control-label {
|
||||
font-weight: 600;
|
||||
|
||||
31
web/sass-files/sass/partials/_webhooks.scss
Normal file
31
web/sass-files/sass/partials/_webhooks.scss
Normal file
@@ -0,0 +1,31 @@
|
||||
.webhooks__container {
|
||||
background: rgba(black, 0.1);
|
||||
border: 1px solid;
|
||||
@include border-radius(3px);
|
||||
padding: 0 13px 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.webhook__item {
|
||||
font-size: 13px;
|
||||
position: relative;
|
||||
&:last-child {
|
||||
.divider-light:last-child {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.webhook__remove {
|
||||
position: absolute;
|
||||
right: -7px;
|
||||
top: 8px;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: #E05F5D;
|
||||
}
|
||||
.webhook__url {
|
||||
padding-right: 20px;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user