Merge pull request #1321 from hmhealey/plt911
PLT-911 Search autocomplete improvements
Этот коммит содержится в:
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
const AsyncClient = require('../utils/async_client.jsx');
|
const AsyncClient = require('../utils/async_client.jsx');
|
||||||
const Client = require('../utils/client.jsx');
|
const Client = require('../utils/client.jsx');
|
||||||
|
const Utils = require('../utils/utils.jsx');
|
||||||
|
|
||||||
const Modal = ReactBootstrap.Modal;
|
const Modal = ReactBootstrap.Modal;
|
||||||
|
|
||||||
export default class EditChannelPurposeModal extends React.Component {
|
export default class EditChannelPurposeModal extends React.Component {
|
||||||
@@ -75,11 +77,6 @@ export default class EditChannelPurposeModal extends React.Component {
|
|||||||
title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>;
|
title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let channelTerm = 'Channel';
|
|
||||||
if (this.props.channel.channelType === 'P') {
|
|
||||||
channelTerm = 'Group';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
className='modal-edit-channel-purpose'
|
className='modal-edit-channel-purpose'
|
||||||
@@ -93,7 +90,7 @@ export default class EditChannelPurposeModal extends React.Component {
|
|||||||
</Modal.Title>
|
</Modal.Title>
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<p>{`Describe how this ${channelTerm} should be used.`}</p>
|
<p>{`Describe how this ${Utils.getChannelTerm(this.props.channel.channelType)} should be used.`}</p>
|
||||||
<textarea
|
<textarea
|
||||||
ref='purpose'
|
ref='purpose'
|
||||||
className='form-control no-resize'
|
className='form-control no-resize'
|
||||||
|
|||||||
@@ -3,14 +3,15 @@
|
|||||||
|
|
||||||
const ChannelStore = require('../stores/channel_store.jsx');
|
const ChannelStore = require('../stores/channel_store.jsx');
|
||||||
const KeyCodes = require('../utils/constants.jsx').KeyCodes;
|
const KeyCodes = require('../utils/constants.jsx').KeyCodes;
|
||||||
|
const Popover = ReactBootstrap.Popover;
|
||||||
const UserStore = require('../stores/user_store.jsx');
|
const UserStore = require('../stores/user_store.jsx');
|
||||||
const Utils = require('../utils/utils.jsx');
|
const Utils = require('../utils/utils.jsx');
|
||||||
|
const Constants = require('../utils/constants.jsx');
|
||||||
|
|
||||||
const patterns = new Map([
|
const patterns = new Map([
|
||||||
['channels', /\b(?:in|channel):\s*(\S*)$/i],
|
['channels', /\b(?:in|channel):\s*(\S*)$/i],
|
||||||
['users', /\bfrom:\s*(\S*)$/i]
|
['users', /\bfrom:\s*(\S*)$/i]
|
||||||
]);
|
]);
|
||||||
const Popover = ReactBootstrap.Popover;
|
|
||||||
|
|
||||||
export default class SearchAutocomplete extends React.Component {
|
export default class SearchAutocomplete extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -22,8 +23,13 @@ export default class SearchAutocomplete extends React.Component {
|
|||||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||||
|
|
||||||
this.completeWord = this.completeWord.bind(this);
|
this.completeWord = this.completeWord.bind(this);
|
||||||
|
this.getSelection = this.getSelection.bind(this);
|
||||||
|
this.scrollToItem = this.scrollToItem.bind(this);
|
||||||
this.updateSuggestions = this.updateSuggestions.bind(this);
|
this.updateSuggestions = this.updateSuggestions.bind(this);
|
||||||
|
|
||||||
|
this.renderChannelSuggestion = this.renderChannelSuggestion.bind(this);
|
||||||
|
this.renderUserSuggestion = this.renderUserSuggestion.bind(this);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
show: false,
|
show: false,
|
||||||
mode: '',
|
mode: '',
|
||||||
@@ -37,9 +43,18 @@ export default class SearchAutocomplete extends React.Component {
|
|||||||
$(document).on('click', this.handleDocumentClick);
|
$(document).on('click', this.handleDocumentClick);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate(prevProps, prevState) {
|
||||||
$(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').perfectScrollbar();
|
const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content');
|
||||||
$(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content').css('max-height', $(window).height() - 200);
|
|
||||||
|
if (this.state.show) {
|
||||||
|
if (!prevState.show) {
|
||||||
|
content.perfectScrollbar();
|
||||||
|
content.css('max-height', $(window).height() - 200);
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep the keyboard selection visible when scrolling
|
||||||
|
this.scrollToItem(this.getSelection());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
@@ -51,7 +66,7 @@ export default class SearchAutocomplete extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleDocumentClick(e) {
|
handleDocumentClick(e) {
|
||||||
const container = $(ReactDOM.findDOMNode(this.refs.container));
|
const container = $(ReactDOM.findDOMNode(this.refs.searchPopover));
|
||||||
|
|
||||||
if (!(container.is(e.target) || container.has(e.target).length > 0)) {
|
if (!(container.is(e.target) || container.has(e.target).length > 0)) {
|
||||||
this.setState({
|
this.setState({
|
||||||
@@ -111,15 +126,7 @@ export default class SearchAutocomplete extends React.Component {
|
|||||||
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) {
|
} else if (e.which === KeyCodes.ENTER || e.which === KeyCodes.SPACE) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
this.completeSelectedWord();
|
this.completeWord(this.getSelection());
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,6 +142,40 @@ export default class SearchAutocomplete extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getSelection() {
|
||||||
|
if (this.state.mode === 'channels') {
|
||||||
|
return this.state.suggestions[this.state.selection].name;
|
||||||
|
} else if (this.state.mode === 'users') {
|
||||||
|
return this.state.suggestions[this.state.selection].username;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
scrollToItem(itemName) {
|
||||||
|
const content = $(ReactDOM.findDOMNode(this.refs.searchPopover)).find('.popover-content');
|
||||||
|
const visibleContentHeight = content[0].clientHeight;
|
||||||
|
const actualContentHeight = content[0].scrollHeight;
|
||||||
|
|
||||||
|
if (this.state.suggestions.length > 0 && visibleContentHeight < actualContentHeight) {
|
||||||
|
const contentTop = content.scrollTop();
|
||||||
|
const contentTopPadding = parseInt(content.css('padding-top'), 10);
|
||||||
|
const contentBottomPadding = parseInt(content.css('padding-top'), 10);
|
||||||
|
|
||||||
|
const item = $(this.refs[itemName]);
|
||||||
|
const itemTop = item[0].offsetTop - parseInt(item.css('margin-top'), 10);
|
||||||
|
const itemBottom = item[0].offsetTop + item.height() + parseInt(item.css('margin-bottom'), 10);
|
||||||
|
|
||||||
|
if (itemTop - contentTopPadding < contentTop) {
|
||||||
|
// the item is off the top of the visible space
|
||||||
|
content.scrollTop(itemTop - contentTopPadding);
|
||||||
|
} else if (itemBottom + contentTopPadding + contentBottomPadding > contentTop + visibleContentHeight) {
|
||||||
|
// the item has gone off the bottom of the visible space
|
||||||
|
content.scrollTop(itemBottom - visibleContentHeight + contentTopPadding + contentBottomPadding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
updateSuggestions(mode, filter) {
|
updateSuggestions(mode, filter) {
|
||||||
let suggestions = [];
|
let suggestions = [];
|
||||||
|
|
||||||
@@ -193,6 +234,46 @@ export default class SearchAutocomplete extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderChannelSuggestion(channel) {
|
||||||
|
let className = 'search-autocomplete__item';
|
||||||
|
if (channel.name === this.getSelection()) {
|
||||||
|
className += ' selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={channel.name}
|
||||||
|
ref={channel.name}
|
||||||
|
onClick={this.handleClick.bind(this, channel.name)}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
{channel.name}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderUserSuggestion(user) {
|
||||||
|
let className = 'search-autocomplete__item';
|
||||||
|
if (user.username === this.getSelection()) {
|
||||||
|
className += ' selected';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={user.username}
|
||||||
|
ref={user.username}
|
||||||
|
onClick={this.handleClick.bind(this, user.username)}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className='profile-img rounded'
|
||||||
|
src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
|
||||||
|
/>
|
||||||
|
{user.username}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
if (!this.state.show || this.state.suggestions.length === 0) {
|
if (!this.state.show || this.state.suggestions.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
@@ -201,45 +282,33 @@ export default class SearchAutocomplete extends React.Component {
|
|||||||
let suggestions = [];
|
let suggestions = [];
|
||||||
|
|
||||||
if (this.state.mode === 'channels') {
|
if (this.state.mode === 'channels') {
|
||||||
suggestions = this.state.suggestions.map((channel, index) => {
|
const publicChannels = this.state.suggestions.filter((channel) => channel.type === Constants.OPEN_CHANNEL);
|
||||||
let className = 'search-autocomplete__item';
|
if (publicChannels.length > 0) {
|
||||||
if (this.state.selection === index) {
|
suggestions.push(
|
||||||
className += ' selected';
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
key={channel.name}
|
key='public-channel-divider'
|
||||||
ref={channel.name}
|
className='search-autocomplete__divider'
|
||||||
onClick={this.handleClick.bind(this, channel.name)}
|
|
||||||
className={className}
|
|
||||||
>
|
>
|
||||||
{channel.name}
|
{'Public ' + Utils.getChannelTerm(Constants.OPEN_CHANNEL) + 's'}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
suggestions = suggestions.concat(publicChannels.map(this.renderChannelSuggestion));
|
||||||
|
}
|
||||||
|
|
||||||
|
const privateChannels = this.state.suggestions.filter((channel) => channel.type === Constants.PRIVATE_CHANNEL);
|
||||||
|
if (privateChannels.length > 0) {
|
||||||
|
suggestions.push(
|
||||||
|
<div
|
||||||
|
key='private-channel-divider'
|
||||||
|
className='search-autocomplete__divider'
|
||||||
|
>
|
||||||
|
{'Private ' + Utils.getChannelTerm(Constants.PRIVATE_CHANNEL) + 's'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
suggestions = suggestions.concat(privateChannels.map(this.renderChannelSuggestion));
|
||||||
|
}
|
||||||
} else if (this.state.mode === 'users') {
|
} else if (this.state.mode === 'users') {
|
||||||
suggestions = this.state.suggestions.map((user, index) => {
|
suggestions = this.state.suggestions.map(this.renderUserSuggestion);
|
||||||
let className = 'search-autocomplete__item';
|
|
||||||
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 rounded'
|
|
||||||
src={'/api/v1/users/' + user.id + '/image?time=' + user.update_at}
|
|
||||||
/>
|
|
||||||
{user.username}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1129,3 +1129,12 @@ export function sortByDisplayName(a, b) {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getChannelTerm(channelType) {
|
||||||
|
let channelTerm = 'Channel';
|
||||||
|
if (channelType === Constants.PRIVATE_CHANNEL) {
|
||||||
|
channelTerm = 'Group';
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelTerm;
|
||||||
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user