diff options
author | David Lu <david.lu@hotmail.com> | 2016-06-02 15:45:38 -0700 |
---|---|---|
committer | enahum <nahumhbl@gmail.com> | 2016-06-02 19:45:38 -0300 |
commit | 9509398d3270c1b6056ca78ddc9913273585c0af (patch) | |
tree | 66b4aca294e91817a0cae9929456c63dd64b4cc6 /webapp/components | |
parent | 2f7540e174dce808dc642c42d85151238c352e5d (diff) | |
download | chat-9509398d3270c1b6056ca78ddc9913273585c0af.tar.gz chat-9509398d3270c1b6056ca78ddc9913273585c0af.tar.bz2 chat-9509398d3270c1b6056ca78ddc9913273585c0af.zip |
PLT-2962 Added channel switcher modal (#3216)
* Added channel switcher modal
* Fixed typos
* Added handling for duplicate channels
Diffstat (limited to 'webapp/components')
-rw-r--r-- | webapp/components/channel_switch_modal.jsx | 153 | ||||
-rw-r--r-- | webapp/components/navbar.jsx | 30 | ||||
-rw-r--r-- | webapp/components/suggestion/suggestion_box.jsx | 9 | ||||
-rw-r--r-- | webapp/components/suggestion/suggestion_list.jsx | 10 | ||||
-rw-r--r-- | webapp/components/suggestion/switch_channel_provider.jsx | 51 |
5 files changed, 248 insertions, 5 deletions
diff --git a/webapp/components/channel_switch_modal.jsx b/webapp/components/channel_switch_modal.jsx new file mode 100644 index 000000000..4194e7b53 --- /dev/null +++ b/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 +}; + diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx index ee199fc03..cfda38670 100644 --- a/webapp/components/navbar.jsx +++ b/webapp/components/navbar.jsx @@ -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> ); } diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx index f81cc6765..0ed9449ed 100644 --- a/webapp/components/suggestion/suggestion_box.jsx +++ b/webapp/components/suggestion/suggestion_box.jsx @@ -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, diff --git a/webapp/components/suggestion/suggestion_list.jsx b/webapp/components/suggestion/suggestion_list.jsx index 7774f9a7d..134e7a8d4 100644 --- a/webapp/components/suggestion/suggestion_list.jsx +++ b/webapp/components/suggestion/suggestion_list.jsx @@ -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 }; diff --git a/webapp/components/suggestion/switch_channel_provider.jsx b/webapp/components/suggestion/switch_channel_provider.jsx new file mode 100644 index 000000000..b52cd7fe9 --- /dev/null +++ b/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); + } + } +} |