PLT-2962 Added channel switcher modal (#3216)

* Added channel switcher modal

* Fixed typos

* Added handling for duplicate channels
Этот коммит содержится в:
David Lu
2016-06-02 15:45:38 -07:00
коммит произвёл enahum
родитель 2f7540e174
Коммит 9509398d32
8 изменённых файлов: 257 добавлений и 6 удалений

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
};

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

@@ -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;
}
}