PLT-2962 Added channel switcher modal (#3216)
* Added channel switcher modal * Fixed typos * Added handling for duplicate channels
Этот коммит содержится в:
153
webapp/components/channel_switch_modal.jsx
Обычный файл
153
webapp/components/channel_switch_modal.jsx
Обычный файл
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import SuggestionList from './suggestion/suggestion_list.jsx';
|
||||
import SuggestionBox from './suggestion/suggestion_box.jsx';
|
||||
import SwitchChannelProvider from './suggestion/switch_channel_provider.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import * as ChannelActions from 'actions/channel_actions.jsx';
|
||||
import React from 'react';
|
||||
|
||||
export default class SwitchChannelModal extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onUserInput = this.onUserInput.bind(this);
|
||||
this.onShow = this.onShow.bind(this);
|
||||
this.onHide = this.onHide.bind(this);
|
||||
this.handleKeyDown = this.handleKeyDown.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.suggestionProviders = [new SwitchChannelProvider()];
|
||||
|
||||
this.state = {
|
||||
text: '',
|
||||
error: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.show && !prevProps.show) {
|
||||
const textbox = this.refs.search.getTextbox();
|
||||
textbox.focus();
|
||||
Utils.placeCaretAtEnd(textbox);
|
||||
}
|
||||
}
|
||||
|
||||
onShow() {
|
||||
this.setState({
|
||||
text: '',
|
||||
error: ''
|
||||
});
|
||||
}
|
||||
|
||||
onHide() {
|
||||
this.setState({
|
||||
text: '',
|
||||
error: ''
|
||||
});
|
||||
this.props.onHide();
|
||||
}
|
||||
|
||||
onUserInput(message) {
|
||||
this.setState({text: message});
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
this.setState({
|
||||
error: ''
|
||||
});
|
||||
if (e.keyCode === Constants.KeyCodes.ENTER) {
|
||||
this.handleSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit() {
|
||||
const channel = ChannelStore.getByName(this.state.text.trim());
|
||||
if (channel !== null && channel.name === this.state.text.trim() && channel.type !== Constants.DM_CHANNEL) {
|
||||
ChannelActions.goToChannel(channel);
|
||||
this.onHide();
|
||||
} else if (this.state.text !== '') {
|
||||
this.setState({
|
||||
error: Utils.localizeMessage('channel_switch_modal.not_found', 'No matches found.')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
let message = this.state.error;
|
||||
return (
|
||||
<Modal
|
||||
className='modal-browse-channel'
|
||||
ref='modal'
|
||||
show={this.props.show}
|
||||
onHide={this.onHide}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id='channel_switch_modal.title'
|
||||
defaultMessage='Switch Channels'
|
||||
/>
|
||||
</span>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
||||
<Modal.Body>
|
||||
<FormattedMessage
|
||||
id='channel_switch_modal.help'
|
||||
defaultMessage='↑↓ to browse, TAB to select, ↵ to confirm, ESC to dismiss'
|
||||
/>
|
||||
<SuggestionBox
|
||||
ref='search'
|
||||
className='form-control focused'
|
||||
type='input'
|
||||
onUserInput={this.onUserInput}
|
||||
value={this.state.text}
|
||||
onKeyDown={this.handleKeyDown}
|
||||
listComponent={SuggestionList}
|
||||
maxLength='64'
|
||||
providers={this.suggestionProviders}
|
||||
preventDefaultSubmit={false}
|
||||
listStyle='bottom'
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<label className='control-label'>
|
||||
{message}
|
||||
</label>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={this.onHide}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='edit_channel_header_modal.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary'
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='channel_switch_modal.submit'
|
||||
defaultMessage='Switch'
|
||||
/>
|
||||
</button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SwitchChannelModal.propTypes = {
|
||||
show: React.PropTypes.bool.isRequired,
|
||||
onHide: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@ import UserStore from 'stores/user_store.jsx';
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import TeamStore from 'stores/team_store.jsx';
|
||||
|
||||
import ChannelSwitchModal from './channel_switch_modal.jsx';
|
||||
|
||||
import Client from 'utils/web_client.jsx';
|
||||
import * as AsyncClient from 'utils/async_client.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
@@ -50,11 +52,15 @@ export default class Navbar extends React.Component {
|
||||
this.createCollapseButtons = this.createCollapseButtons.bind(this);
|
||||
this.createDropdown = this.createDropdown.bind(this);
|
||||
|
||||
this.showChannelSwitchModal = this.showChannelSwitchModal.bind(this);
|
||||
this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this);
|
||||
|
||||
const state = this.getStateFromStores();
|
||||
state.showEditChannelPurposeModal = false;
|
||||
state.showEditChannelHeaderModal = false;
|
||||
state.showMembersModal = false;
|
||||
state.showRenameChannelModal = false;
|
||||
state.showChannelSwitchModal = false;
|
||||
this.state = state;
|
||||
}
|
||||
getStateFromStores() {
|
||||
@@ -72,10 +78,12 @@ export default class Navbar extends React.Component {
|
||||
ChannelStore.addChangeListener(this.onChange);
|
||||
ChannelStore.addExtraInfoChangeListener(this.onChange);
|
||||
$('.inner-wrap').click(this.hideSidebars);
|
||||
document.addEventListener('keydown', this.showChannelSwitchModal);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onChange);
|
||||
ChannelStore.removeExtraInfoChangeListener(this.onChange);
|
||||
document.removeEventListener('keydown', this.showChannelSwitchModal);
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
@@ -150,6 +158,19 @@ export default class Navbar extends React.Component {
|
||||
showRenameChannelModal: false
|
||||
});
|
||||
}
|
||||
showChannelSwitchModal(e) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.keyCode === Constants.KeyCodes.K) {
|
||||
e.preventDefault();
|
||||
this.setState({
|
||||
showChannelSwitchModal: true
|
||||
});
|
||||
}
|
||||
}
|
||||
hideChannelSwitchModal() {
|
||||
this.setState({
|
||||
showChannelSwitchModal: false
|
||||
});
|
||||
}
|
||||
createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) {
|
||||
if (channel) {
|
||||
var viewInfoOption = (
|
||||
@@ -441,6 +462,7 @@ export default class Navbar extends React.Component {
|
||||
var editChannelPurposeModal = null;
|
||||
let renameChannelModal = null;
|
||||
let channelMembersModal = null;
|
||||
let channelSwitchModal = null;
|
||||
|
||||
if (channel) {
|
||||
popoverContent = (
|
||||
@@ -540,6 +562,13 @@ export default class Navbar extends React.Component {
|
||||
channel={channel}
|
||||
/>
|
||||
);
|
||||
|
||||
channelSwitchModal = (
|
||||
<ChannelSwitchModal
|
||||
show={this.state.showChannelSwitchModal}
|
||||
onHide={this.hideChannelSwitchModal}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
var collapseButtons = this.createCollapseButtons(currentId);
|
||||
@@ -574,6 +603,7 @@ export default class Navbar extends React.Component {
|
||||
{editChannelPurposeModal}
|
||||
{renameChannelModal}
|
||||
{channelMembersModal}
|
||||
{channelSwitchModal}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -176,7 +176,10 @@ export default class SuggestionBox extends React.Component {
|
||||
return (
|
||||
<div>
|
||||
{textbox}
|
||||
<SuggestionListComponent suggestionId={this.suggestionId}/>
|
||||
<SuggestionListComponent
|
||||
suggestionId={this.suggestionId}
|
||||
location={this.props.listStyle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -197,7 +200,8 @@ export default class SuggestionBox extends React.Component {
|
||||
}
|
||||
|
||||
SuggestionBox.defaultProps = {
|
||||
type: 'input'
|
||||
type: 'input',
|
||||
listStyle: 'top'
|
||||
};
|
||||
|
||||
SuggestionBox.propTypes = {
|
||||
@@ -206,6 +210,7 @@ SuggestionBox.propTypes = {
|
||||
value: React.PropTypes.string.isRequired,
|
||||
onUserInput: React.PropTypes.func,
|
||||
providers: React.PropTypes.arrayOf(React.PropTypes.object),
|
||||
listStyle: React.PropTypes.string,
|
||||
|
||||
// explicitly name any input event handlers we override and need to manually call
|
||||
onChange: React.PropTypes.func,
|
||||
|
||||
@@ -117,11 +117,14 @@ export default class SuggestionList extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
const mainClass = 'suggestion-list suggestion-list--' + this.props.location;
|
||||
const contentClass = 'suggestion-list__content suggestion-list__content--' + this.props.location;
|
||||
|
||||
return (
|
||||
<div className='suggestion-list suggestion-list--top'>
|
||||
<div className={mainClass}>
|
||||
<div
|
||||
ref='content'
|
||||
className='suggestion-list__content suggestion-list__content--top'
|
||||
className={contentClass}
|
||||
>
|
||||
{items}
|
||||
</div>
|
||||
@@ -131,5 +134,6 @@ export default class SuggestionList extends React.Component {
|
||||
}
|
||||
|
||||
SuggestionList.propTypes = {
|
||||
suggestionId: React.PropTypes.string.isRequired
|
||||
suggestionId: React.PropTypes.string.isRequired,
|
||||
location: React.PropTypes.string
|
||||
};
|
||||
|
||||
51
webapp/components/suggestion/switch_channel_provider.jsx
Обычный файл
51
webapp/components/suggestion/switch_channel_provider.jsx
Обычный файл
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import ChannelStore from 'stores/channel_store.jsx';
|
||||
import SuggestionStore from 'stores/suggestion_store.jsx';
|
||||
import Suggestion from './suggestion.jsx';
|
||||
|
||||
class SwitchChannelSuggestion extends Suggestion {
|
||||
render() {
|
||||
const {item, isSelection} = this.props;
|
||||
|
||||
let className = 'mentions__name';
|
||||
if (isSelection) {
|
||||
className += ' suggestion--selected';
|
||||
}
|
||||
|
||||
const displayName = item.display_name + ' (' + item.name + ')';
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={this.handleClick}
|
||||
className={className}
|
||||
>
|
||||
{displayName}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default class SwitchChannelProvider {
|
||||
handlePretextChanged(suggestionId, channelPrefix) {
|
||||
if (channelPrefix) {
|
||||
const allChannels = ChannelStore.getAll();
|
||||
const channels = [];
|
||||
|
||||
for (const id of Object.keys(allChannels)) {
|
||||
const channel = allChannels[id];
|
||||
if (channel.display_name.toLowerCase().startsWith(channelPrefix.toLowerCase())) {
|
||||
channels.push(channel);
|
||||
}
|
||||
}
|
||||
|
||||
channels.sort((a, b) => a.display_name.localeCompare(b.display_name));
|
||||
const channelNames = channels.map((channel) => channel.name);
|
||||
|
||||
SuggestionStore.addSuggestions(suggestionId, channelNames, channels, SwitchChannelSuggestion, channelPrefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -715,6 +715,10 @@
|
||||
"channel_notifications.sendDesktop": "Send desktop notifications",
|
||||
"channel_notifications.unreadInfo": "The channel name is bolded in the sidebar when there are unread messages. Selecting \"Only for mentions\" will bold the channel only when you are mentioned.",
|
||||
"channel_select.placeholder": "--- Select a channel ---",
|
||||
"channel_switch_modal.title": "Switch Channels",
|
||||
"channel_switch_modal.help": "↑↓ to browse, TAB to select, ↵ to confirm, ESC to dismiss",
|
||||
"channel_switch_modal.submit": "Switch",
|
||||
"channel_switch_modal.not_found": "No matches found.",
|
||||
"choose_auth_page.emailCreate": "Create new team with email address",
|
||||
"choose_auth_page.find": "Find my teams",
|
||||
"choose_auth_page.gitlabCreate": "Create new team with GitLab Account",
|
||||
|
||||
@@ -36,6 +36,7 @@ class ChannelStoreClass extends EventEmitter {
|
||||
this.get = this.get.bind(this);
|
||||
this.getMember = this.getMember.bind(this);
|
||||
this.getByName = this.getByName.bind(this);
|
||||
this.getByDisplayName = this.getByDisplayName.bind(this);
|
||||
this.setPostMode = this.setPostMode.bind(this);
|
||||
this.getPostMode = this.getPostMode.bind(this);
|
||||
this.setUnreadCount = this.setUnreadCount.bind(this);
|
||||
@@ -118,6 +119,9 @@ class ChannelStoreClass extends EventEmitter {
|
||||
getByName(name) {
|
||||
return this.findFirstBy('name', name);
|
||||
}
|
||||
getByDisplayName(displayName) {
|
||||
return this.findFirstBy('display_name', displayName);
|
||||
}
|
||||
getMoreByName(name) {
|
||||
return this.findFirstMoreBy('name', name);
|
||||
}
|
||||
|
||||
@@ -29,4 +29,4 @@ export function getProfilePicSrcForPost(post, timestamp) {
|
||||
}
|
||||
|
||||
return src;
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user