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 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
|
||||||
};
|
};
|
||||||
|
|||||||
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.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;
|
||||||
}
|
}
|
||||||
Ссылка в новой задаче
Block a user