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 ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx'; import TeamStore from 'stores/team_store.jsx';
import ChannelSwitchModal from './channel_switch_modal.jsx';
import Client from 'utils/web_client.jsx'; import Client from 'utils/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx'; import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.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.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this); this.createDropdown = this.createDropdown.bind(this);
this.showChannelSwitchModal = this.showChannelSwitchModal.bind(this);
this.hideChannelSwitchModal = this.hideChannelSwitchModal.bind(this);
const state = this.getStateFromStores(); const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false; state.showEditChannelPurposeModal = false;
state.showEditChannelHeaderModal = false; state.showEditChannelHeaderModal = false;
state.showMembersModal = false; state.showMembersModal = false;
state.showRenameChannelModal = false; state.showRenameChannelModal = false;
state.showChannelSwitchModal = false;
this.state = state; this.state = state;
} }
getStateFromStores() { getStateFromStores() {
@@ -72,10 +78,12 @@ export default class Navbar extends React.Component {
ChannelStore.addChangeListener(this.onChange); ChannelStore.addChangeListener(this.onChange);
ChannelStore.addExtraInfoChangeListener(this.onChange); ChannelStore.addExtraInfoChangeListener(this.onChange);
$('.inner-wrap').click(this.hideSidebars); $('.inner-wrap').click(this.hideSidebars);
document.addEventListener('keydown', this.showChannelSwitchModal);
} }
componentWillUnmount() { componentWillUnmount() {
ChannelStore.removeChangeListener(this.onChange); ChannelStore.removeChangeListener(this.onChange);
ChannelStore.removeExtraInfoChangeListener(this.onChange); ChannelStore.removeExtraInfoChangeListener(this.onChange);
document.removeEventListener('keydown', this.showChannelSwitchModal);
} }
handleSubmit(e) { handleSubmit(e) {
e.preventDefault(); e.preventDefault();
@@ -150,6 +158,19 @@ export default class Navbar extends React.Component {
showRenameChannelModal: false 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) { createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent) {
if (channel) { if (channel) {
var viewInfoOption = ( var viewInfoOption = (
@@ -441,6 +462,7 @@ export default class Navbar extends React.Component {
var editChannelPurposeModal = null; var editChannelPurposeModal = null;
let renameChannelModal = null; let renameChannelModal = null;
let channelMembersModal = null; let channelMembersModal = null;
let channelSwitchModal = null;
if (channel) { if (channel) {
popoverContent = ( popoverContent = (
@@ -540,6 +562,13 @@ export default class Navbar extends React.Component {
channel={channel} channel={channel}
/> />
); );
channelSwitchModal = (
<ChannelSwitchModal
show={this.state.showChannelSwitchModal}
onHide={this.hideChannelSwitchModal}
/>
);
} }
var collapseButtons = this.createCollapseButtons(currentId); var collapseButtons = this.createCollapseButtons(currentId);
@@ -574,6 +603,7 @@ export default class Navbar extends React.Component {
{editChannelPurposeModal} {editChannelPurposeModal}
{renameChannelModal} {renameChannelModal}
{channelMembersModal} {channelMembersModal}
{channelSwitchModal}
</div> </div>
); );
} }

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

@@ -176,7 +176,10 @@ export default class SuggestionBox extends React.Component {
return ( return (
<div> <div>
{textbox} {textbox}
<SuggestionListComponent suggestionId={this.suggestionId}/> <SuggestionListComponent
suggestionId={this.suggestionId}
location={this.props.listStyle}
/>
</div> </div>
); );
} }
@@ -197,7 +200,8 @@ export default class SuggestionBox extends React.Component {
} }
SuggestionBox.defaultProps = { SuggestionBox.defaultProps = {
type: 'input' type: 'input',
listStyle: 'top'
}; };
SuggestionBox.propTypes = { SuggestionBox.propTypes = {
@@ -206,6 +210,7 @@ SuggestionBox.propTypes = {
value: React.PropTypes.string.isRequired, value: React.PropTypes.string.isRequired,
onUserInput: React.PropTypes.func, onUserInput: React.PropTypes.func,
providers: React.PropTypes.arrayOf(React.PropTypes.object), providers: React.PropTypes.arrayOf(React.PropTypes.object),
listStyle: React.PropTypes.string,
// explicitly name any input event handlers we override and need to manually call // explicitly name any input event handlers we override and need to manually call
onChange: React.PropTypes.func, 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 ( return (
<div className='suggestion-list suggestion-list--top'> <div className={mainClass}>
<div <div
ref='content' ref='content'
className='suggestion-list__content suggestion-list__content--top' className={contentClass}
> >
{items} {items}
</div> </div>
@@ -131,5 +134,6 @@ export default class SuggestionList extends React.Component {
} }
SuggestionList.propTypes = { 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.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_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_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.emailCreate": "Create new team with email address",
"choose_auth_page.find": "Find my teams", "choose_auth_page.find": "Find my teams",
"choose_auth_page.gitlabCreate": "Create new team with GitLab Account", "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.get = this.get.bind(this);
this.getMember = this.getMember.bind(this); this.getMember = this.getMember.bind(this);
this.getByName = this.getByName.bind(this); this.getByName = this.getByName.bind(this);
this.getByDisplayName = this.getByDisplayName.bind(this);
this.setPostMode = this.setPostMode.bind(this); this.setPostMode = this.setPostMode.bind(this);
this.getPostMode = this.getPostMode.bind(this); this.getPostMode = this.getPostMode.bind(this);
this.setUnreadCount = this.setUnreadCount.bind(this); this.setUnreadCount = this.setUnreadCount.bind(this);
@@ -118,6 +119,9 @@ class ChannelStoreClass extends EventEmitter {
getByName(name) { getByName(name) {
return this.findFirstBy('name', name); return this.findFirstBy('name', name);
} }
getByDisplayName(displayName) {
return this.findFirstBy('display_name', displayName);
}
getMoreByName(name) { getMoreByName(name) {
return this.findFirstMoreBy('name', name); return this.findFirstMoreBy('name', name);
} }

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

@@ -29,4 +29,4 @@ export function getProfilePicSrcForPost(post, timestamp) {
} }
return src; return src;
} }