Merge pull request #1199 from hmhealey/plt600

PLT-600 Renamed channel description to channel header and added channel purpose field
Этот коммит содержится в:
Corey Hulen
2015-10-28 14:44:02 -07:00
родитель 9312272234 019bf6a7fe
Коммит 89f67cd11d
20 изменённых файлов: 535 добавлений и 201 удалений

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

@@ -22,7 +22,8 @@ func InitChannel(r *mux.Router) {
sr.Handle("/create", ApiUserRequired(createChannel)).Methods("POST")
sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST")
sr.Handle("/update", ApiUserRequired(updateChannel)).Methods("POST")
sr.Handle("/update_desc", ApiUserRequired(updateChannelDesc)).Methods("POST")
sr.Handle("/update_header", ApiUserRequired(updateChannelHeader)).Methods("POST")
sr.Handle("/update_purpose", ApiUserRequired(updateChannelPurpose)).Methods("POST")
sr.Handle("/update_notify_props", ApiUserRequired(updateNotifyProps)).Methods("POST")
sr.Handle("/{id:[A-Za-z0-9]+}/", ApiUserRequiredActivity(getChannel, false)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).Methods("GET")
@@ -124,7 +125,7 @@ func CreateDirectChannel(c *Context, otherUserId string) (*model.Channel, *model
channel.Name = model.GetDMNameFromIds(otherUserId, c.Session.UserId)
channel.TeamId = c.Session.TeamId
channel.Description = ""
channel.Header = ""
channel.Type = model.CHANNEL_DIRECT
if uresult := <-uc; uresult.Err != nil {
@@ -209,7 +210,8 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
oldChannel.Description = channel.Description
oldChannel.Header = channel.Header
oldChannel.Purpose = channel.Purpose
if len(channel.DisplayName) > 0 {
oldChannel.DisplayName = channel.DisplayName
@@ -233,18 +235,18 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
func updateChannelDesc(c *Context, w http.ResponseWriter, r *http.Request) {
func updateChannelHeader(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
channelId := props["channel_id"]
if len(channelId) != 26 {
c.SetInvalidParam("updateChannelDesc", "channel_id")
c.SetInvalidParam("updateChannelHeader", "channel_id")
return
}
channelDesc := props["channel_description"]
if len(channelDesc) > 1024 {
c.SetInvalidParam("updateChannelDesc", "channel_description")
channelHeader := props["channel_header"]
if len(channelHeader) > 1024 {
c.SetInvalidParam("updateChannelHeader", "channel_header")
return
}
@@ -261,11 +263,54 @@ func updateChannelDesc(c *Context, w http.ResponseWriter, r *http.Request) {
channel := cresult.Data.(*model.Channel)
// Don't need to do anything channel member, just wanted to confirm it exists
if !c.HasPermissionsToTeam(channel.TeamId, "updateChannelDesc") {
if !c.HasPermissionsToTeam(channel.TeamId, "updateChannelHeader") {
return
}
channel.Description = channelDesc
channel.Header = channelHeader
if ucresult := <-Srv.Store.Channel().Update(channel); ucresult.Err != nil {
c.Err = ucresult.Err
return
} else {
c.LogAudit("name=" + channel.Name)
w.Write([]byte(channel.ToJson()))
}
}
}
func updateChannelPurpose(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
channelId := props["channel_id"]
if len(channelId) != 26 {
c.SetInvalidParam("updateChannelPurpose", "channel_id")
return
}
channelPurpose := props["channel_purpose"]
if len(channelPurpose) > 1024 {
c.SetInvalidParam("updateChannelPurpose", "channel_purpose")
return
}
sc := Srv.Store.Channel().Get(channelId)
cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId)
if cresult := <-sc; cresult.Err != nil {
c.Err = cresult.Err
return
} else if cmcresult := <-cmc; cmcresult.Err != nil {
c.Err = cmcresult.Err
return
} else {
channel := cresult.Data.(*model.Channel)
// Don't need to do anything channel member, just wanted to confirm it exists
if !c.HasPermissionsToTeam(channel.TeamId, "updateChannelPurpose") {
return
}
channel.Purpose = channelPurpose
if ucresult := <-Srv.Store.Channel().Update(channel); ucresult.Err != nil {
c.Err = ucresult.Err

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

@@ -61,8 +61,8 @@ func BenchmarkCreateDirectChannel(b *testing.B) {
func BenchmarkUpdateChannel(b *testing.B) {
var (
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
CHANNEL_DESCRIPTION_LEN = 50
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
CHANNEL_HEADER_LEN = 50
)
team, _, _ := SetupBenchmark()
@@ -73,7 +73,7 @@ func BenchmarkUpdateChannel(b *testing.B) {
}
for i := range channels {
channels[i].Description = utils.RandString(CHANNEL_DESCRIPTION_LEN, utils.ALPHANUMERIC)
channels[i].Header = utils.RandString(CHANNEL_HEADER_LEN, utils.ALPHANUMERIC)
}
// Benchmark Start

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

@@ -8,6 +8,7 @@ import (
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
"net/http"
"strings"
"testing"
"time"
)
@@ -173,12 +174,17 @@ func TestUpdateChannel(t *testing.T) {
Client.AddChannelMember(channel1.Id, userTeamAdmin.Id)
desc := "a" + model.NewId() + "a"
upChannel1 := &model.Channel{Id: channel1.Id, Description: desc}
header := "a" + model.NewId() + "a"
purpose := "a" + model.NewId() + "a"
upChannel1 := &model.Channel{Id: channel1.Id, Header: header, Purpose: purpose}
upChannel1 = Client.Must(Client.UpdateChannel(upChannel1)).Data.(*model.Channel)
if upChannel1.Description != desc {
t.Fatal("Channel admin failed to update desc")
if upChannel1.Header != header {
t.Fatal("Channel admin failed to update header")
}
if upChannel1.Purpose != purpose {
t.Fatal("Channel admin failed to update purpose")
}
if upChannel1.DisplayName != channel1.DisplayName {
@@ -187,12 +193,17 @@ func TestUpdateChannel(t *testing.T) {
Client.LoginByEmail(team.Name, userTeamAdmin.Email, "pwd")
desc = "b" + model.NewId() + "b"
upChannel1 = &model.Channel{Id: channel1.Id, Description: desc}
header = "b" + model.NewId() + "b"
purpose = "b" + model.NewId() + "b"
upChannel1 = &model.Channel{Id: channel1.Id, Header: header, Purpose: purpose}
upChannel1 = Client.Must(Client.UpdateChannel(upChannel1)).Data.(*model.Channel)
if upChannel1.Description != desc {
t.Fatal("Team admin failed to update desc")
if upChannel1.Header != header {
t.Fatal("Team admin failed to update header")
}
if upChannel1.Purpose != purpose {
t.Fatal("Team admin failed to update purpose")
}
if upChannel1.DisplayName != channel1.DisplayName {
@@ -203,7 +214,7 @@ func TestUpdateChannel(t *testing.T) {
data := rget.Data.(*model.ChannelList)
for _, c := range data.Channels {
if c.Name == model.DEFAULT_CHANNEL {
c.Description = "new desc"
c.Header = "new header"
if _, err := Client.UpdateChannel(c); err == nil {
t.Fatal("should have errored on updating default channel")
}
@@ -218,7 +229,7 @@ func TestUpdateChannel(t *testing.T) {
}
}
func TestUpdateChannelDesc(t *testing.T) {
func TestUpdateChannelHeader(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
@@ -235,36 +246,33 @@ func TestUpdateChannelDesc(t *testing.T) {
data := make(map[string]string)
data["channel_id"] = channel1.Id
data["channel_description"] = "new desc"
data["channel_header"] = "new header"
var upChannel1 *model.Channel
if result, err := Client.UpdateChannelDesc(data); err != nil {
if result, err := Client.UpdateChannelHeader(data); err != nil {
t.Fatal(err)
} else {
upChannel1 = result.Data.(*model.Channel)
}
if upChannel1.Description != data["channel_description"] {
t.Fatal("Failed to update desc")
if upChannel1.Header != data["channel_header"] {
t.Fatal("Failed to update header")
}
data["channel_id"] = "junk"
if _, err := Client.UpdateChannelDesc(data); err == nil {
if _, err := Client.UpdateChannelHeader(data); err == nil {
t.Fatal("should have errored on junk channel id")
}
data["channel_id"] = "12345678901234567890123456"
if _, err := Client.UpdateChannelDesc(data); err == nil {
if _, err := Client.UpdateChannelHeader(data); err == nil {
t.Fatal("should have errored on non-existent channel id")
}
data["channel_id"] = channel1.Id
data["channel_description"] = ""
for i := 0; i < 1050; i++ {
data["channel_description"] += "a"
}
if _, err := Client.UpdateChannelDesc(data); err == nil {
t.Fatal("should have errored on bad channel desc")
data["channel_header"] = strings.Repeat("a", 1050)
if _, err := Client.UpdateChannelHeader(data); err == nil {
t.Fatal("should have errored on bad channel header")
}
user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
@@ -274,9 +282,68 @@ func TestUpdateChannelDesc(t *testing.T) {
Client.LoginByEmail(team.Name, user2.Email, "pwd")
data["channel_id"] = channel1.Id
data["channel_description"] = "new desc"
if _, err := Client.UpdateChannelDesc(data); err == nil {
t.Fatal("should have errored non-channel member trying to update desc")
data["channel_header"] = "new header"
if _, err := Client.UpdateChannelHeader(data); err == nil {
t.Fatal("should have errored non-channel member trying to update header")
}
}
func TestUpdateChannelPurpose(t *testing.T) {
Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user.Id))
Client.LoginByEmail(team.Name, user.Email, "pwd")
channel1 := &model.Channel{DisplayName: "A Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
data := make(map[string]string)
data["channel_id"] = channel1.Id
data["channel_purpose"] = "new purpose"
var upChannel1 *model.Channel
if result, err := Client.UpdateChannelPurpose(data); err != nil {
t.Fatal(err)
} else {
upChannel1 = result.Data.(*model.Channel)
}
if upChannel1.Purpose != data["channel_purpose"] {
t.Fatal("Failed to update purpose")
}
data["channel_id"] = "junk"
if _, err := Client.UpdateChannelPurpose(data); err == nil {
t.Fatal("should have errored on junk channel id")
}
data["channel_id"] = "12345678901234567890123456"
if _, err := Client.UpdateChannelPurpose(data); err == nil {
t.Fatal("should have errored on non-existent channel id")
}
data["channel_id"] = channel1.Id
data["channel_purpose"] = strings.Repeat("a", 150)
if _, err := Client.UpdateChannelPurpose(data); err == nil {
t.Fatal("should have errored on bad channel purpose")
}
user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(user2.Id))
Client.LoginByEmail(team.Name, user2.Email, "pwd")
data["channel_id"] = channel1.Id
data["channel_purpose"] = "new purpose"
if _, err := Client.UpdateChannelPurpose(data); err == nil {
t.Fatal("should have errored non-channel member trying to update purpose")
}
}

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

@@ -182,7 +182,7 @@ func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[str
Type: model.CHANNEL_OPEN,
DisplayName: sChannel.Name,
Name: SlackConvertChannelName(sChannel.Name),
Description: sChannel.Topic["value"],
Purpose: sChannel.Topic["value"],
}
mChannel := ImportChannel(&newChannel)
if mChannel == nil {

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

@@ -24,7 +24,8 @@ type Channel struct {
Type string `json:"type"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
Description string `json:"description"`
Header string `json:"header"`
Purpose string `json:"purpose"`
LastPostAt int64 `json:"last_post_at"`
TotalMsgCount int64 `json:"total_msg_count"`
ExtraUpdateAt int64 `json:"extra_update_at"`
@@ -89,8 +90,12 @@ func (o *Channel) IsValid() *AppError {
return NewAppError("Channel.IsValid", "Invalid type", "id="+o.Id)
}
if len(o.Description) > 1024 {
return NewAppError("Channel.IsValid", "Invalid description", "id="+o.Id)
if len(o.Header) > 1024 {
return NewAppError("Channel.IsValid", "Invalid header", "id="+o.Id)
}
if len(o.Purpose) > 128 {
return NewAppError("Channel.IsValid", "Invalid purpose", "id="+o.Id)
}
if len(o.CreatorId) > 26 {

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

@@ -67,6 +67,26 @@ func TestChannelIsValid(t *testing.T) {
if err := o.IsValid(); err != nil {
t.Fatal(err)
}
o.Header = strings.Repeat("01234567890", 100)
if err := o.IsValid(); err == nil {
t.Fatal("should be invalid")
}
o.Header = "1234"
if err := o.IsValid(); err != nil {
t.Fatal(err)
}
o.Purpose = strings.Repeat("01234567890", 20)
if err := o.IsValid(); err == nil {
t.Fatal("should be invalid")
}
o.Purpose = "1234"
if err := o.IsValid(); err != nil {
t.Fatal(err)
}
}
func TestChannelPreSave(t *testing.T) {

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

@@ -452,8 +452,17 @@ func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) {
}
}
func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/update_desc", MapToJson(data)); err != nil {
func (c *Client) UpdateChannelHeader(data map[string]string) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/update_header", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), ChannelFromJson(r.Body)}, nil
}
}
func (c *Client) UpdateChannelPurpose(data map[string]string) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/update_purpose", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),

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

@@ -25,7 +25,8 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
table.ColMap("DisplayName").SetMaxSize(64)
table.ColMap("Name").SetMaxSize(64)
table.SetUniqueTogether("Name", "TeamId")
table.ColMap("Description").SetMaxSize(1024)
table.ColMap("Header").SetMaxSize(1024)
table.ColMap("Purpose").SetMaxSize(128)
table.ColMap("CreatorId").SetMaxSize(26)
tablem := db.AddTableWithName(model.ChannelMember{}, "ChannelMembers").SetKeys(false, "ChannelId", "UserId")
@@ -83,6 +84,11 @@ func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
s.RemoveColumnIfExists("ChannelMembers", "NotifyLevel")
}
// BEGIN REMOVE AFTER 1.2.0
s.RenameColumnIfExists("Channels", "Description", "Header", "varchar(1024)")
s.CreateColumnIfNotExists("Channels", "Purpose", "varchar(1024)", "varchar(1024)", "")
// END REMOVE AFTER 1.2.0
}
func (s SqlChannelStore) CreateIndexesIfNotExists() {

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

@@ -364,27 +364,26 @@ func (ss SqlStore) RemoveColumnIfExists(tableName string, columnName string) boo
return true
}
// func (ss SqlStore) RenameColumnIfExists(tableName string, oldColumnName string, newColumnName string, colType string) bool {
func (ss SqlStore) RenameColumnIfExists(tableName string, oldColumnName string, newColumnName string, colType string) bool {
if !ss.DoesColumnExist(tableName, oldColumnName) {
return false
}
// // XXX TODO FIXME this should be removed after 0.6.0
// if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
// return false
// }
var err error
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
_, err = ss.GetMaster().Exec("ALTER TABLE " + tableName + " CHANGE " + oldColumnName + " " + newColumnName + " " + colType)
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
_, err = ss.GetMaster().Exec("ALTER TABLE " + tableName + " RENAME COLUMN " + oldColumnName + " TO " + newColumnName)
}
// if !ss.DoesColumnExist(tableName, oldColumnName) {
// return false
// }
if err != nil {
l4g.Critical("Failed to rename column %v", err)
time.Sleep(time.Second)
panic("Failed to drop column " + err.Error())
}
// _, err := ss.GetMaster().Exec("ALTER TABLE " + tableName + " CHANGE " + oldColumnName + " " + newColumnName + " " + colType)
// if err != nil {
// l4g.Critical("Failed to rename column %v", err)
// time.Sleep(time.Second)
// panic("Failed to drop column " + err.Error())
// }
// return true
// }
return true
}
func (ss SqlStore) CreateIndexIfNotExists(indexName string, tableName string, columnName string) {
ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_DEFAULT)

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

@@ -90,8 +90,9 @@ export default class AccessHistoryModal extends React.Component {
case '/channels/update':
currentAuditDesc = 'Updated the ' + channelName + ' channel/group name';
break;
case '/channels/update_desc':
currentAuditDesc = 'Updated the ' + channelName + ' channel/group description';
case '/channels/update_desc': // support the old path
case '/channels/update_header':
currentAuditDesc = 'Updated the ' + channelName + ' channel/group header';
break;
default:
let userIdField = [];

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

@@ -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 {
@@ -110,11 +113,11 @@ export default class ChannelHeader extends React.Component {
bSize='large'
placement='bottom'
className='description'
onMouseOver={() => this.refs.descriptionOverlay.show()}
onMouseOut={() => this.refs.descriptionOverlay.hide()}
onMouseOver={() => this.refs.headerOverlay.show()}
onMouseOut={() => this.refs.headerOverlay.hide()}
>
<MessageWrapper
message={channel.description}
message={channel.header}
/>
</Popover>
);
@@ -144,7 +147,7 @@ export default class ChannelHeader extends React.Component {
if (isDirect) {
dropdownContents.push(
<li
key='edit_description_direct'
key='edit_header_direct'
role='presentation'
>
<a
@@ -152,11 +155,11 @@ export default class ChannelHeader extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
data-desc={channel.description}
data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
Set Channel Description...
Set Channel Header...
</a>
</li>
);
@@ -216,7 +219,7 @@ export default class ChannelHeader extends React.Component {
dropdownContents.push(
<li
key='set_channel_description'
key='set_channel_header'
role='presentation'
>
<a
@@ -224,11 +227,25 @@ export default class ChannelHeader extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
data-desc={channel.description}
data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
Set {channelTerm} Description...
Set {channelTerm} Header...
</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>
);
@@ -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='descriptionOverlay'
>
<div
onClick={TextFormatting.handleClick}
className='description'
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {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>
);
}
}

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

@@ -14,7 +14,7 @@ export default class EditChannelModal extends React.Component {
this.onShow = this.onShow.bind(this);
this.state = {
description: '',
header: '',
title: '',
channelId: '',
serverError: ''
@@ -28,32 +28,32 @@ export default class EditChannelModal extends React.Component {
return;
}
data.channel_description = this.state.description.trim();
data.channel_header = this.state.header.trim();
Client.updateChannelDesc(data,
function handleUpdateSuccess() {
Client.updateChannelHeader(data,
() => {
this.setState({serverError: ''});
AsyncClient.getChannel(this.state.channelId);
$(ReactDOM.findDOMNode(this.refs.modal)).modal('hide');
}.bind(this),
function handleUpdateError(err) {
if (err.message === 'Invalid channel_description parameter') {
this.setState({serverError: 'This description is too long, please enter a shorter one'});
},
(err) => {
if (err.message === 'Invalid channel_header parameter') {
this.setState({serverError: 'This channel header is too long, please enter a shorter one'});
} else {
this.setState({serverError: err.message});
}
}.bind(this)
}
);
}
handleUserInput(e) {
this.setState({description: e.target.value});
this.setState({header: e.target.value});
}
handleClose() {
this.setState({description: '', serverError: ''});
this.setState({header: '', serverError: ''});
}
onShow(e) {
const button = e.relatedTarget;
this.setState({description: $(button).attr('data-desc'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
this.setState({header: $(button).attr('data-header'), title: $(button).attr('data-title'), channelId: $(button).attr('data-channelid'), serverError: ''});
}
componentDidMount() {
$(ReactDOM.findDOMNode(this.refs.modal)).on('show.bs.modal', this.onShow);
@@ -73,7 +73,7 @@ export default class EditChannelModal extends React.Component {
className='modal-title'
ref='title'
>
Edit Description
Edit Header
</h4>
);
if (this.state.title) {
@@ -82,7 +82,7 @@ export default class EditChannelModal extends React.Component {
className='modal-title'
ref='title'
>
Edit Description for <span className='name'>{this.state.title}</span>
Edit Header for <span className='name'>{this.state.title}</span>
</h4>
);
}
@@ -113,9 +113,8 @@ export default class EditChannelModal extends React.Component {
<textarea
className='form-control no-resize'
rows='6'
ref='channelDesc'
maxLength='1024'
value={this.state.description}
value={this.state.header}
onChange={this.handleUserInput}
/>
{serverError}

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

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

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

@@ -109,7 +109,7 @@ export default class MoreChannels extends React.Component {
<tr key={channel.id}>
<td>
<p className='more-name'>{channel.display_name}</p>
<p className='more-description'>{channel.description}</p>
<p className='more-purpose'>{channel.purpose}</p>
</td>
<td className='td--action'>
{joinButton}

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

@@ -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 {
@@ -106,22 +109,35 @@ export default class Navbar extends React.Component {
</li>
);
var setChannelDescriptionOption = (
var setChannelHeaderOption = (
<li role='presentation'>
<a
role='menuitem'
href='#'
data-toggle='modal'
data-target='#edit_channel'
data-desc={channel.description}
data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
Set Channel Description...
Set Channel Header...
</a>
</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)) {
@@ -249,7 +265,8 @@ export default class Navbar extends React.Component {
{viewInfoOption}
{addMembersOption}
{manageMembersOption}
{setChannelDescriptionOption}
{setChannelHeaderOption}
{setChannelPurposeOption}
{notificationPreferenceOption}
{renameChannelOption}
{deleteChannelOption}
@@ -335,10 +352,10 @@ export default class Navbar extends React.Component {
<Popover
bsStyle='info'
placement='bottom'
id='description-popover'
id='header-popover'
>
<MessageWrapper
message={channel.description}
message={channel.header}
options={{singleline: true, mentionHighlight: false}}
/>
</Popover>
@@ -360,20 +377,20 @@ export default class Navbar extends React.Component {
}
}
if (channel.description.length === 0) {
if (channel.header.length === 0) {
popoverContent = (
<Popover
bsStyle='info'
placement='bottom'
id='description-popover'
id='header-popover'
>
<div>
{'No channel description yet.'}
{'No channel header yet.'}
<br/>
<a
href='#'
data-toggle='modal'
data-desc={channel.description}
data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
data-target='#edit_channel'
@@ -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>
);
}
}

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

@@ -30,7 +30,7 @@ export default class NewChannelFlow extends React.Component {
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
channelDescription: '',
channelPurpose: '',
nameModified: false
};
}
@@ -43,7 +43,7 @@ export default class NewChannelFlow extends React.Component {
flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '',
channelName: '',
channelDescription: '',
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.description = this.state.channelDescription;
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,
channelDescription: data.description
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,
description: this.state.channelDescription
purpose: this.state.channelPurpose
};
let showChannelModal = false;

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

@@ -36,7 +36,7 @@ export default class NewChannelModal extends React.Component {
handleChange() {
const newData = {
displayName: ReactDOM.findDOMNode(this.refs.display_name).value,
description: ReactDOM.findDOMNode(this.refs.channel_desc).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'>{'Description'}</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_desc'
ref='channel_purpose'
rows='4'
placeholder='Description'
maxLength='1024'
value={this.props.channelData.description}
placeholder='Purpose'
maxLength='128'
value={this.props.channelData.purpose}
onChange={this.handleChange}
tabIndex='2'
/>
<p className='input__help'>
{'Description helps others decide whether to join this channel.'}
{`Describe how this ${channelTerm} should be used.`}
</p>
{serverError}
</div>

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

@@ -358,11 +358,11 @@ export default class PostList extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
data-desc={channel.description}
data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
<i className='fa fa-pencil'></i>{'Set a description'}
<i className='fa fa-pencil'></i>{'Set a header'}
</a>
</div>
);
@@ -413,11 +413,11 @@ export default class PostList extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
data-desc={channel.description}
data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
<i className='fa fa-pencil'></i>{'Set a description'}
<i className='fa fa-pencil'></i>{'Set a header'}
</a>
<a
className='intro-links'
@@ -479,11 +479,11 @@ export default class PostList extends React.Component {
href='#'
data-toggle='modal'
data-target='#edit_channel'
data-desc={channel.description}
data-header={channel.header}
data-title={channel.display_name}
data-channelid={channel.id}
>
<i className='fa fa-pencil'></i>{'Set a description'}
<i className='fa fa-pencil'></i>{'Set a header'}
</a>
<a
className='intro-links'

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

@@ -589,21 +589,38 @@ export function updateChannel(channel, success, error) {
track('api', 'api_channels_update');
}
export function updateChannelDesc(data, success, error) {
export function updateChannelHeader(data, success, error) {
$.ajax({
url: '/api/v1/channels/update_desc',
url: '/api/v1/channels/update_header',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(data),
success,
error: function onError(xhr, status, err) {
var e = handleError('updateChannelDesc', xhr, status, err);
var e = handleError('updateChannelHeader', xhr, status, err);
error(e);
}
});
track('api', 'api_channels_desc');
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) {

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

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