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", ApiUserRequired(createChannel)).Methods("POST")
sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST") sr.Handle("/create_direct", ApiUserRequired(createDirectChannel)).Methods("POST")
sr.Handle("/update", ApiUserRequired(updateChannel)).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("/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]+}/", ApiUserRequiredActivity(getChannel, false)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/extra_info", ApiUserRequired(getChannelExtraInfo)).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.Name = model.GetDMNameFromIds(otherUserId, c.Session.UserId)
channel.TeamId = c.Session.TeamId channel.TeamId = c.Session.TeamId
channel.Description = "" channel.Header = ""
channel.Type = model.CHANNEL_DIRECT channel.Type = model.CHANNEL_DIRECT
if uresult := <-uc; uresult.Err != nil { if uresult := <-uc; uresult.Err != nil {
@@ -209,7 +210,8 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
oldChannel.Description = channel.Description oldChannel.Header = channel.Header
oldChannel.Purpose = channel.Purpose
if len(channel.DisplayName) > 0 { if len(channel.DisplayName) > 0 {
oldChannel.DisplayName = channel.DisplayName 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) props := model.MapFromJson(r.Body)
channelId := props["channel_id"] channelId := props["channel_id"]
if len(channelId) != 26 { if len(channelId) != 26 {
c.SetInvalidParam("updateChannelDesc", "channel_id") c.SetInvalidParam("updateChannelHeader", "channel_id")
return return
} }
channelDesc := props["channel_description"] channelHeader := props["channel_header"]
if len(channelDesc) > 1024 { if len(channelHeader) > 1024 {
c.SetInvalidParam("updateChannelDesc", "channel_description") c.SetInvalidParam("updateChannelHeader", "channel_header")
return return
} }
@@ -261,11 +263,54 @@ func updateChannelDesc(c *Context, w http.ResponseWriter, r *http.Request) {
channel := cresult.Data.(*model.Channel) channel := cresult.Data.(*model.Channel)
// Don't need to do anything channel member, just wanted to confirm it exists // 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 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 { if ucresult := <-Srv.Store.Channel().Update(channel); ucresult.Err != nil {
c.Err = ucresult.Err c.Err = ucresult.Err

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

@@ -61,8 +61,8 @@ func BenchmarkCreateDirectChannel(b *testing.B) {
func BenchmarkUpdateChannel(b *testing.B) { func BenchmarkUpdateChannel(b *testing.B) {
var ( var (
NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS} NUM_CHANNELS_RANGE = utils.Range{NUM_CHANNELS, NUM_CHANNELS}
CHANNEL_DESCRIPTION_LEN = 50 CHANNEL_HEADER_LEN = 50
) )
team, _, _ := SetupBenchmark() team, _, _ := SetupBenchmark()
@@ -73,7 +73,7 @@ func BenchmarkUpdateChannel(b *testing.B) {
} }
for i := range channels { 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 // Benchmark Start

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

@@ -8,6 +8,7 @@ import (
"github.com/mattermost/platform/store" "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils" "github.com/mattermost/platform/utils"
"net/http" "net/http"
"strings"
"testing" "testing"
"time" "time"
) )
@@ -173,12 +174,17 @@ func TestUpdateChannel(t *testing.T) {
Client.AddChannelMember(channel1.Id, userTeamAdmin.Id) Client.AddChannelMember(channel1.Id, userTeamAdmin.Id)
desc := "a" + model.NewId() + "a" header := "a" + model.NewId() + "a"
upChannel1 := &model.Channel{Id: channel1.Id, Description: desc} purpose := "a" + model.NewId() + "a"
upChannel1 := &model.Channel{Id: channel1.Id, Header: header, Purpose: purpose}
upChannel1 = Client.Must(Client.UpdateChannel(upChannel1)).Data.(*model.Channel) upChannel1 = Client.Must(Client.UpdateChannel(upChannel1)).Data.(*model.Channel)
if upChannel1.Description != desc { if upChannel1.Header != header {
t.Fatal("Channel admin failed to update desc") 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 { if upChannel1.DisplayName != channel1.DisplayName {
@@ -187,12 +193,17 @@ func TestUpdateChannel(t *testing.T) {
Client.LoginByEmail(team.Name, userTeamAdmin.Email, "pwd") Client.LoginByEmail(team.Name, userTeamAdmin.Email, "pwd")
desc = "b" + model.NewId() + "b" header = "b" + model.NewId() + "b"
upChannel1 = &model.Channel{Id: channel1.Id, Description: desc} purpose = "b" + model.NewId() + "b"
upChannel1 = &model.Channel{Id: channel1.Id, Header: header, Purpose: purpose}
upChannel1 = Client.Must(Client.UpdateChannel(upChannel1)).Data.(*model.Channel) upChannel1 = Client.Must(Client.UpdateChannel(upChannel1)).Data.(*model.Channel)
if upChannel1.Description != desc { if upChannel1.Header != header {
t.Fatal("Team admin failed to update desc") 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 { if upChannel1.DisplayName != channel1.DisplayName {
@@ -203,7 +214,7 @@ func TestUpdateChannel(t *testing.T) {
data := rget.Data.(*model.ChannelList) data := rget.Data.(*model.ChannelList)
for _, c := range data.Channels { for _, c := range data.Channels {
if c.Name == model.DEFAULT_CHANNEL { if c.Name == model.DEFAULT_CHANNEL {
c.Description = "new desc" c.Header = "new header"
if _, err := Client.UpdateChannel(c); err == nil { if _, err := Client.UpdateChannel(c); err == nil {
t.Fatal("should have errored on updating default channel") 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() Setup()
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} 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 := make(map[string]string)
data["channel_id"] = channel1.Id data["channel_id"] = channel1.Id
data["channel_description"] = "new desc" data["channel_header"] = "new header"
var upChannel1 *model.Channel var upChannel1 *model.Channel
if result, err := Client.UpdateChannelDesc(data); err != nil { if result, err := Client.UpdateChannelHeader(data); err != nil {
t.Fatal(err) t.Fatal(err)
} else { } else {
upChannel1 = result.Data.(*model.Channel) upChannel1 = result.Data.(*model.Channel)
} }
if upChannel1.Description != data["channel_description"] { if upChannel1.Header != data["channel_header"] {
t.Fatal("Failed to update desc") t.Fatal("Failed to update header")
} }
data["channel_id"] = "junk" 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") t.Fatal("should have errored on junk channel id")
} }
data["channel_id"] = "12345678901234567890123456" 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") t.Fatal("should have errored on non-existent channel id")
} }
data["channel_id"] = channel1.Id data["channel_id"] = channel1.Id
data["channel_description"] = "" data["channel_header"] = strings.Repeat("a", 1050)
for i := 0; i < 1050; i++ { if _, err := Client.UpdateChannelHeader(data); err == nil {
data["channel_description"] += "a" t.Fatal("should have errored on bad channel header")
}
if _, err := Client.UpdateChannelDesc(data); err == nil {
t.Fatal("should have errored on bad channel desc")
} }
user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"} 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") Client.LoginByEmail(team.Name, user2.Email, "pwd")
data["channel_id"] = channel1.Id data["channel_id"] = channel1.Id
data["channel_description"] = "new desc" data["channel_header"] = "new header"
if _, err := Client.UpdateChannelDesc(data); err == nil { if _, err := Client.UpdateChannelHeader(data); err == nil {
t.Fatal("should have errored non-channel member trying to update desc") 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, Type: model.CHANNEL_OPEN,
DisplayName: sChannel.Name, DisplayName: sChannel.Name,
Name: SlackConvertChannelName(sChannel.Name), Name: SlackConvertChannelName(sChannel.Name),
Description: sChannel.Topic["value"], Purpose: sChannel.Topic["value"],
} }
mChannel := ImportChannel(&newChannel) mChannel := ImportChannel(&newChannel)
if mChannel == nil { if mChannel == nil {

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

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

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

@@ -67,6 +67,26 @@ func TestChannelIsValid(t *testing.T) {
if err := o.IsValid(); err != nil { if err := o.IsValid(); err != nil {
t.Fatal(err) 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) { 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) { func (c *Client) UpdateChannelHeader(data map[string]string) (*Result, *AppError) {
if r, err := c.DoApiPost("/channels/update_desc", MapToJson(data)); err != nil { 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 return nil, err
} else { } else {
return &Result{r.Header.Get(HEADER_REQUEST_ID), return &Result{r.Header.Get(HEADER_REQUEST_ID),

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

@@ -25,7 +25,8 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
table.ColMap("DisplayName").SetMaxSize(64) table.ColMap("DisplayName").SetMaxSize(64)
table.ColMap("Name").SetMaxSize(64) table.ColMap("Name").SetMaxSize(64)
table.SetUniqueTogether("Name", "TeamId") table.SetUniqueTogether("Name", "TeamId")
table.ColMap("Description").SetMaxSize(1024) table.ColMap("Header").SetMaxSize(1024)
table.ColMap("Purpose").SetMaxSize(128)
table.ColMap("CreatorId").SetMaxSize(26) table.ColMap("CreatorId").SetMaxSize(26)
tablem := db.AddTableWithName(model.ChannelMember{}, "ChannelMembers").SetKeys(false, "ChannelId", "UserId") tablem := db.AddTableWithName(model.ChannelMember{}, "ChannelMembers").SetKeys(false, "ChannelId", "UserId")
@@ -83,6 +84,11 @@ func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
s.RemoveColumnIfExists("ChannelMembers", "NotifyLevel") 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() { func (s SqlChannelStore) CreateIndexesIfNotExists() {

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

@@ -364,27 +364,26 @@ func (ss SqlStore) RemoveColumnIfExists(tableName string, columnName string) boo
return true 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 var err error
// if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES { if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
// return false _, 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) { if err != nil {
// return false 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) return true
}
// if err != nil {
// l4g.Critical("Failed to rename column %v", err)
// time.Sleep(time.Second)
// panic("Failed to drop column " + err.Error())
// }
// return true
// }
func (ss SqlStore) CreateIndexIfNotExists(indexName string, tableName string, columnName string) { func (ss SqlStore) CreateIndexIfNotExists(indexName string, tableName string, columnName string) {
ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_DEFAULT) ss.createIndexIfNotExists(indexName, tableName, columnName, INDEX_TYPE_DEFAULT)

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

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

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

@@ -11,6 +11,7 @@ const TextFormatting = require('../utils/text_formatting.jsx');
const Utils = require('../utils/utils.jsx'); const Utils = require('../utils/utils.jsx');
const MessageWrapper = require('./message_wrapper.jsx'); const MessageWrapper = require('./message_wrapper.jsx');
const PopoverListMembers = require('./popover_list_members.jsx'); const PopoverListMembers = require('./popover_list_members.jsx');
const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx'); const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
const Constants = require('../utils/constants.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.handleLeave = this.handleLeave.bind(this);
this.searchMentions = this.searchMentions.bind(this); this.searchMentions = this.searchMentions.bind(this);
this.state = this.getStateFromStores(); const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
this.state = state;
} }
getStateFromStores() { getStateFromStores() {
return { return {
@@ -110,11 +113,11 @@ export default class ChannelHeader extends React.Component {
bSize='large' bSize='large'
placement='bottom' placement='bottom'
className='description' className='description'
onMouseOver={() => this.refs.descriptionOverlay.show()} onMouseOver={() => this.refs.headerOverlay.show()}
onMouseOut={() => this.refs.descriptionOverlay.hide()} onMouseOut={() => this.refs.headerOverlay.hide()}
> >
<MessageWrapper <MessageWrapper
message={channel.description} message={channel.header}
/> />
</Popover> </Popover>
); );
@@ -144,7 +147,7 @@ export default class ChannelHeader extends React.Component {
if (isDirect) { if (isDirect) {
dropdownContents.push( dropdownContents.push(
<li <li
key='edit_description_direct' key='edit_header_direct'
role='presentation' role='presentation'
> >
<a <a
@@ -152,11 +155,11 @@ export default class ChannelHeader extends React.Component {
href='#' href='#'
data-toggle='modal' data-toggle='modal'
data-target='#edit_channel' data-target='#edit_channel'
data-desc={channel.description} data-header={channel.header}
data-title={channel.display_name} data-title={channel.display_name}
data-channelid={channel.id} data-channelid={channel.id}
> >
Set Channel Description... Set Channel Header...
</a> </a>
</li> </li>
); );
@@ -216,7 +219,7 @@ export default class ChannelHeader extends React.Component {
dropdownContents.push( dropdownContents.push(
<li <li
key='set_channel_description' key='set_channel_header'
role='presentation' role='presentation'
> >
<a <a
@@ -224,11 +227,25 @@ export default class ChannelHeader extends React.Component {
href='#' href='#'
data-toggle='modal' data-toggle='modal'
data-target='#edit_channel' data-target='#edit_channel'
data-desc={channel.description} data-header={channel.header}
data-title={channel.display_name} data-title={channel.display_name}
data-channelid={channel.id} 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> </a>
</li> </li>
); );
@@ -307,84 +324,91 @@ export default class ChannelHeader extends React.Component {
} }
return ( return (
<table className='channel-header alt'> <div>
<tbody> <table className='channel-header alt'>
<tr> <tbody>
<th> <tr>
<div className='channel-header__info'> <th>
<div className='dropdown'> <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 <a
href='#' href='#'
className='dropdown-toggle theme' className='dropdown-toggle theme'
type='button' type='button'
id='channel_header_dropdown' id='channel_header_right_dropdown'
data-toggle='dropdown' data-toggle='dropdown'
aria-expanded='true' aria-expanded='true'
> >
<strong className='heading'>{channelTitle} </strong> <span dangerouslySetInnerHTML={{__html: Constants.MENU_ICON}} />
<span className='glyphicon glyphicon-chevron-down header-dropdown__icon' />
</a> </a>
<ul <ul
className='dropdown-menu' className='dropdown-menu dropdown-menu-right'
role='menu' 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> </ul>
</div> </div>
<OverlayTrigger </th>
trigger={['hover', 'focus']} </tr>
placement='bottom' </tbody>
overlay={popoverContent} </table>
ref='descriptionOverlay' <EditChannelPurposeModal
> show={this.state.showEditChannelPurposeModal}
<div onModalDismissed={() => this.setState({showEditChannelPurposeModal: false})}
onClick={TextFormatting.handleClick} channel={channel}
className='description' />
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(channel.description, {singleline: true, mentionHighlight: false})}} </div>
/>
</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>
); );
} }
} }

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

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

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

@@ -8,6 +8,7 @@ var ChannelStore = require('../stores/channel_store.jsx');
var TeamStore = require('../stores/team_store.jsx'); var TeamStore = require('../stores/team_store.jsx');
var MessageWrapper = require('./message_wrapper.jsx'); var MessageWrapper = require('./message_wrapper.jsx');
var NotifyCounts = require('./notify_counts.jsx'); var NotifyCounts = require('./notify_counts.jsx');
const EditChannelPurposeModal = require('./edit_channel_purpose_modal.jsx');
const Utils = require('../utils/utils.jsx'); const Utils = require('../utils/utils.jsx');
var Constants = require('../utils/constants.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.createCollapseButtons = this.createCollapseButtons.bind(this);
this.createDropdown = this.createDropdown.bind(this); this.createDropdown = this.createDropdown.bind(this);
this.state = this.getStateFromStores(); const state = this.getStateFromStores();
state.showEditChannelPurposeModal = false;
this.state = state;
} }
getStateFromStores() { getStateFromStores() {
return { return {
@@ -106,22 +109,35 @@ export default class Navbar extends React.Component {
</li> </li>
); );
var setChannelDescriptionOption = ( var setChannelHeaderOption = (
<li role='presentation'> <li role='presentation'>
<a <a
role='menuitem' role='menuitem'
href='#' href='#'
data-toggle='modal' data-toggle='modal'
data-target='#edit_channel' data-target='#edit_channel'
data-desc={channel.description} data-header={channel.header}
data-title={channel.display_name} data-title={channel.display_name}
data-channelid={channel.id} data-channelid={channel.id}
> >
Set Channel Description... Set Channel Header...
</a> </a>
</li> </li>
); );
var setChannelPurposeOption = null;
if (!isDirect) {
setChannelPurposeOption = (
<li role='presentation'>
<a
role='menuitem'
href='#'
onClick={() => this.setState({showEditChannelPurposeModal: true})}
/>
</li>
);
}
var addMembersOption; var addMembersOption;
var leaveChannelOption; var leaveChannelOption;
if (!isDirect && !ChannelStore.isDefault(channel)) { if (!isDirect && !ChannelStore.isDefault(channel)) {
@@ -249,7 +265,8 @@ export default class Navbar extends React.Component {
{viewInfoOption} {viewInfoOption}
{addMembersOption} {addMembersOption}
{manageMembersOption} {manageMembersOption}
{setChannelDescriptionOption} {setChannelHeaderOption}
{setChannelPurposeOption}
{notificationPreferenceOption} {notificationPreferenceOption}
{renameChannelOption} {renameChannelOption}
{deleteChannelOption} {deleteChannelOption}
@@ -335,10 +352,10 @@ export default class Navbar extends React.Component {
<Popover <Popover
bsStyle='info' bsStyle='info'
placement='bottom' placement='bottom'
id='description-popover' id='header-popover'
> >
<MessageWrapper <MessageWrapper
message={channel.description} message={channel.header}
options={{singleline: true, mentionHighlight: false}} options={{singleline: true, mentionHighlight: false}}
/> />
</Popover> </Popover>
@@ -360,20 +377,20 @@ export default class Navbar extends React.Component {
} }
} }
if (channel.description.length === 0) { if (channel.header.length === 0) {
popoverContent = ( popoverContent = (
<Popover <Popover
bsStyle='info' bsStyle='info'
placement='bottom' placement='bottom'
id='description-popover' id='header-popover'
> >
<div> <div>
{'No channel description yet.'} {'No channel header yet.'}
<br/> <br/>
<a <a
href='#' href='#'
data-toggle='modal' data-toggle='modal'
data-desc={channel.description} data-header={channel.header}
data-title={channel.display_name} data-title={channel.display_name}
data-channelid={channel.id} data-channelid={channel.id}
data-target='#edit_channel' data-target='#edit_channel'
@@ -392,17 +409,24 @@ export default class Navbar extends React.Component {
var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent); var channelMenuDropdown = this.createDropdown(channel, channelTitle, isAdmin, isDirect, popoverContent);
return ( return (
<nav <div>
className='navbar navbar-default navbar-fixed-top' <nav
role='navigation' className='navbar navbar-default navbar-fixed-top'
> role='navigation'
<div className='container-fluid theme'> >
<div className='navbar-header'> <div className='container-fluid theme'>
{collapseButtons} <div className='navbar-header'>
{channelMenuDropdown} {collapseButtons}
{channelMenuDropdown}
</div>
</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, flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '', channelDisplayName: '',
channelName: '', channelName: '',
channelDescription: '', channelPurpose: '',
nameModified: false nameModified: false
}; };
} }
@@ -43,7 +43,7 @@ export default class NewChannelFlow extends React.Component {
flowState: SHOW_NEW_CHANNEL, flowState: SHOW_NEW_CHANNEL,
channelDisplayName: '', channelDisplayName: '',
channelName: '', channelName: '',
channelDescription: '', channelPurpose: '',
nameModified: false nameModified: false
}); });
} }
@@ -65,7 +65,7 @@ export default class NewChannelFlow extends React.Component {
const cu = UserStore.getCurrentUser(); const cu = UserStore.getCurrentUser();
channel.team_id = cu.team_id; channel.team_id = cu.team_id;
channel.description = this.state.channelDescription; channel.purpose = this.state.channelPurpose;
channel.type = this.state.channelType; channel.type = this.state.channelType;
Client.createChannel(channel, Client.createChannel(channel,
@@ -109,7 +109,7 @@ export default class NewChannelFlow extends React.Component {
channelDataChanged(data) { channelDataChanged(data) {
this.setState({ this.setState({
channelDisplayName: data.displayName, channelDisplayName: data.displayName,
channelDescription: data.description channelPurpose: data.purpose
}); });
if (!this.state.nameModified) { if (!this.state.nameModified) {
this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())}); this.setState({channelName: Utils.cleanUpUrlable(data.displayName.trim())});
@@ -119,7 +119,7 @@ export default class NewChannelFlow extends React.Component {
const channelData = { const channelData = {
name: this.state.channelName, name: this.state.channelName,
displayName: this.state.channelDisplayName, displayName: this.state.channelDisplayName,
description: this.state.channelDescription purpose: this.state.channelPurpose
}; };
let showChannelModal = false; let showChannelModal = false;

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

@@ -36,7 +36,7 @@ export default class NewChannelModal extends React.Component {
handleChange() { handleChange() {
const newData = { const newData = {
displayName: ReactDOM.findDOMNode(this.refs.display_name).value, 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); this.props.onDataChanged(newData);
} }
@@ -136,22 +136,22 @@ export default class NewChannelModal extends React.Component {
</div> </div>
<div className='form-group less'> <div className='form-group less'>
<div className='col-sm-3'> <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> <label className='form__label light'>{'(optional)'}</label>
</div> </div>
<div className='col-sm-9'> <div className='col-sm-9'>
<textarea <textarea
className='form-control no-resize' className='form-control no-resize'
ref='channel_desc' ref='channel_purpose'
rows='4' rows='4'
placeholder='Description' placeholder='Purpose'
maxLength='1024' maxLength='128'
value={this.props.channelData.description} value={this.props.channelData.purpose}
onChange={this.handleChange} onChange={this.handleChange}
tabIndex='2' tabIndex='2'
/> />
<p className='input__help'> <p className='input__help'>
{'Description helps others decide whether to join this channel.'} {`Describe how this ${channelTerm} should be used.`}
</p> </p>
{serverError} {serverError}
</div> </div>

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

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

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

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

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

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