Added ability to see/edit channel purpose in the client

This commit is contained in:
hmhealey
2015-10-27 12:34:21 -04:00
parent 61da22ea32
commit 019bf6a7fe
8 changed files with 274 additions and 92 deletions

View File

@@ -11,6 +11,7 @@ const TextFormatting = require('../utils/text_formatting.jsx');
const Utils = require('../utils/utils.jsx');
const MessageWrapper = require('./message_wrapper.jsx');
const PopoverListMembers = require('./popover_list_members.jsx');
const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const Constants = require('../utils/constants.jsx');
@@ -27,7 +28,9 @@ export default class ChannelHeader extends React.Component {
this.handleLeave = this.handleLeave.bind(this);
this.searchMentions = this.searchMentions.bind(this);
this.state = this.getStateFromStores();
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
this.state = state;
}
getStateFromStores() {
return {
@@ -232,6 +235,20 @@ export default class ChannelHeader extends React.Component {
</a>
</li>
);
dropdownContents.push(
<li
key='set_channel_purpose'
role='presentation'
>
<a
role='menuitem'
href='#'
onClick={() => this.setState({showEditChannelPurposeModal: true})}
>
Set {channelTerm} Purpose...
</a>
</li>
);
dropdownContents.push(
<li
key='notification_preferences'
@@ -307,84 +324,91 @@ export default class ChannelHeader extends React.Component {
}
return (
<table className='channel-header alt'>
<tbody>
<tr>
<th>
<div className='channel-header__info'>
<div className='dropdown'>
<div>
<table className='channel-header alt'>
<tbody>
<tr>
<th>
<div className='channel-header__info'>
<div className='dropdown'>
<a
href='#'
className='dropdown-toggle theme'
type='button'
id='channel_header_dropdown'
data-toggle='dropdown'
aria-expanded='true'
>
<strong className='heading'>{channelTitle} </strong>
<span className='glyphicon glyphicon-chevron-down header-dropdown__icon' />
</a>
<ul
className='dropdown-menu'
role='menu'
aria-labelledby='channel_header_dropdown'
>
{dropdownContents}
</ul>
</div>
<OverlayTrigger
trigger={['hover', 'focus']}
placement='bottom'
overlay={popoverContent}
ref='headerOverlay'
>
<div
onClick={TextFormatting.handleClick}
className='description'
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}}
/>
</OverlayTrigger>
</div>
</th>
<th>
<PopoverListMembers
members={this.state.users}
channelId={channel.id}
/>
</th>
<th className='search-bar__container'><NavbarSearchBox /></th>
<th>
<div className='dropdown channel-header__links'>
<a
href='#'
className='dropdown-toggle theme'
type='button'
id='channel_header_dropdown'
id='channel_header_right_dropdown'
data-toggle='dropdown'
aria-expanded='true'
>
<strong className='heading'>{channelTitle} </strong>
<span className='glyphicon glyphicon-chevron-down header-dropdown__icon' />
<span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
</a>
<ul
className='dropdown-menu'
className='dropdown-menu dropdown-menu-right'
role='menu'
aria-labelledby='channel_header_dropdown'
aria-labelledby='channel_header_right_dropdown'
>
{dropdownContents}
<li role='presentation'>
<a
role='menuitem'
href='#'
onClick={this.searchMentions}
>
Recent Mentions
</a>
</li>
</ul>
</div>
<OverlayTrigger
trigger={['hover', 'focus']}
placement='bottom'
overlay={popoverContent}
ref='headerOverlay'
>
<div
onClick={TextFormatting.handleClick}
className='description'
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.header, {singleline: true, mentionHighlight: false})}}
/>
</OverlayTrigger>
</div>
</th>
<th>
<PopoverListMembers
members={this.state.users}
channelId={channel.id}
/>
</th>
<th className='search-bar__container'><NavbarSearchBox /></th>
<th>
<div className='dropdown channel-header__links'>
<a
href='#'
className='dropdown-toggle theme'
type='button'
id='channel_header_right_dropdown'
data-toggle='dropdown'
aria-expanded='true'
>
<span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
</a>
<ul
className='dropdown-menu dropdown-menu-right'
role='menu'
aria-labelledby='channel_header_right_dropdown'
>
<li role='presentation'>
<a
role='menuitem'
href='#'
onClick={this.searchMentions}
>
Recent Mentions
</a>
</li>
</ul>
</div>
</th>
</tr>
</tbody>
</table>
</th>
</tr>
</tbody>
</table>
<EditChannelPurposeModal
show={this.state.showEditChannelPurposeModal}
onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
channel={channel}
/>
</div>
);
}
}

View File

@@ -0,0 +1,118 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
const AsyncClient = require('../utils/async_client.jsx');
const Client = require('../utils/client.jsx');
const Modal = ReactBootstrap.Modal;
export default class EditChannelPurposeModal extends React.Component {
constructor(props) {
super(props);
this.handleHide = this.handleHide.bind(this);
this.handleSave = this.handleSave.bind(this);
this.state = {serverError: ''};
}
handleHide() {
this.setState({serverError: ''});
if (this.props.onModalDismissed) {
this.props.onModalDismissed();
}
}
handleSave() {
if (!this.props.channel) {
return;
}
const data = {
channel_id: this.props.channel.id,
channel_purpose: ReactDOM.findDOMNode(this.refs.purpose).value.trim()
};
Client.updateChannelPurpose(data,
() => {
AsyncClient.getChannel(this.props.channel.id);
this.handleHide();
},
(err) => {
if (err.message === 'Invalid channel_purpose parameter') {
this.setState({serverError: 'This channel purpose is too long, please enter a shorter one'});
} else {
this.setState({serverError: err.message});
}
}
);
}
render() {
if (!this.props.show) {
return null;
}
let serverError = null;
if (this.state.serverError) {
serverError = (
<div className='form-group has-error'>
<br/>
<label className='control-label'>{this.state.serverError}</label>
</div>
);
}
let title = <span>{'Edit Purpose'}</span>;
if (this.props.channel.display_name) {
title = <span>{'Edit Purpose for '}<span className='name'>{this.props.channel.display_name}</span></span>;
}
return (
<Modal
className='modal-edit-channel-purpose'
show={this.props.show}
onHide={this.handleHide}
>
<Modal.Header closeButton={true}>
<Modal.Title>
{title}
</Modal.Title>
</Modal.Header>
<Modal.Body>
<textarea
ref='purpose'
className='form-control no-resize'
rows='6'
maxLength='128'
defaultValue={this.props.channel.purpose}
/>
{serverError}
</Modal.Body>
<Modal.Footer>
<button
type='button'
className='btn btn-default'
onClick={this.handleHide}
>
{'Cancel'}
</button>
<button
type='button'
className='btn btn-primary'
onClick={this.handleSave}
>
{'Save'}
</button>
</Modal.Footer>
</Modal>
);
}
}
EditChannelPurposeModal.propTypes = {
show: React.PropTypes.bool.isRequired,
channel: React.PropTypes.object,
onModalDismissed: React.PropTypes.func.isRequired
};

View File

@@ -105,12 +105,11 @@ export default class MoreChannels extends React.Component {
);
}
// TODO Switch channel.header to channel.purpose once that has been added
return (
<tr key={channel.id}>
<td>
<p className='more-name'>{channel.display_name}</p>
<p className='more-header'>{channel.header}</p>
<p className='more-purpose'>{channel.purpose}</p>
</td>
<td className='td--action'>
{joinButton}

View File

@@ -8,6 +8,7 @@ var ChannelStore = require('../stores/channel_store.jsx');
var TeamStore = require('../stores/team_store.jsx');
var MessageWrapper = require('./message_wrapper.jsx');
var NotifyCounts = require('./notify_counts.jsx');
const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
const Utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.jsx');
@@ -26,7 +27,9 @@ export default class Navbar extends React.Component {
this.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this);
this.state = this.getStateFromStores();
const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
this.state = state;
}
getStateFromStores() {
return {
@@ -122,6 +125,19 @@ export default class Navbar extends React.Component {
</li>
);
var setChannelPurposeOption = null;
if (!isDirect) {
setChannelPurposeOption = (
<li role='presentation'>
<a
role='menuitem'
href='#'
onClick={() => this.setState({showEditChannelPurposeModal: true})}
/>
</li>
);
}
var addMembersOption;
var leaveChannelOption;
if (!isDirect && !ChannelStore.isDefault(channel)) {
@@ -250,6 +266,7 @@ export default class Navbar extends React.Component {
{addMembersOption}
{manageMembersOption}
{setChannelHeaderOption}
{setChannelPurposeOption}
{notificationPreferenceOption}
{renameChannelOption}
{deleteChannelOption}
@@ -392,17 +409,24 @@ export default class Navbar extends React.Component {
var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent);
return (
<nav
className='navbar navbar-default navbar-fixed-top'
role='navigation'
>
<div className='container-fluid theme'>
<div className='navbar-header'>
{collapseButtons}
{channelMenuDropdown}
<div>
<nav
className='navbar navbar-default navbar-fixed-top'
role='navigation'
>
<div className='container-fluid theme'>
<div className='navbar-header'>
{collapseButtons}
{channelMenuDropdown}
</div>
</div>
</div>
</nav>
</nav>
<EditChannelPurposeModal
show={this.state.showEditChannelPurposeModal}
onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
channel={channel}
/>
</div>
);
}
}

View File

@@ -30,7 +30,7 @@ export default class NewChannelFlow extends React.Component {
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
channelHeader: '',
channelPurpose: '',
nameModified: false
};
}
@@ -43,7 +43,7 @@ export default class NewChannelFlow extends React.Component {
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
channelHeader: '',
channelPurpose: '',
nameModified: false
});
}
@@ -65,7 +65,7 @@ export default class NewChannelFlow extends React.Component {
const cu = UserStore.getCurrentUser();
channel.team_id = cu.team_id;
channel.header = this.state.channelHeader;
channel.purpose = this.state.channelPurpose;
channel.type = this.state.channelType;
Client.createChannel(channel,
@@ -109,7 +109,7 @@ export default class NewChannelFlow extends React.Component {
channelDataChanged(data) {
this.setState({
channelDisplayName: data.displayName,
channelHeader: data.header
channelPurpose: data.purpose
});
if (!this.state.nameModified) {
this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
@@ -119,7 +119,7 @@ export default class NewChannelFlow extends React.Component {
const channelData = {
name: this.state.channelName,
displayName: this.state.channelDisplayName,
header: this.state.channelHeader
purpose: this.state.channelPurpose
};
let showChannelModal = false;

View File

@@ -36,7 +36,7 @@ export default class NewChannelModal extends React.Component {
handleChange() {
const newData = {
displayName: ReactDOM.findDOMNode(this.refs.display_name).value,
header: ReactDOM.findDOMNode(this.refs.channel_header).value
purpose: ReactDOM.findDOMNode(this.refs.channel_purpose).value
};
this.props.onDataChanged(newData);
}
@@ -136,22 +136,22 @@ export default class NewChannelModal extends React.Component {
</div>
<div className='form-group less'>
<div className='col-sm-3'>
<label className='form__label control-label'>{'Header'}</label>
<label className='form__label control-label'>{'Purpose'}</label>
<label className='form__label light'>{'(optional)'}</label>
</div>
<div className='col-sm-9'>
<textarea
className='form-control no-resize'
ref='channel_header'
ref='channel_purpose'
rows='4'
placeholder='Header'
maxLength='1024'
value={this.props.channelData.header}
placeholder='Purpose'
maxLength='128'
value={this.props.channelData.purpose}
onChange={this.handleChange}
tabIndex='2'
/>
<p className='input__help'>
{'This text is shown in the channel header and supports markdown formatting.'}
{`Describe how this ${channelTerm} should be used.`}
</p>
{serverError}
</div>

View File

@@ -592,6 +592,23 @@ export function updateChannelHeader(data, success, error) {
track('api', 'api_channels_header');
}
export function updateChannelPurpose(data, success, error) {
$.ajax({
url: '/api/v1/channels/update_purpose',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success,
error: function onError(xhr, status, err) {
var e = handleError('updateChannelPurpose', xhr, status, err);
error(e);
}
});
track('api', 'api_channels_purpose');
}
export function updateNotifyProps(data, success, error) {
$.ajax({
url: '/api/v1/channels/update_notify_props',

View File

@@ -377,7 +377,7 @@
@include opacity(0.8);
}
.more-header {
.more-purpose {
@include opacity(0.7);
}