summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCarlos Tadeu Panato Junior <ctadeu@gmail.com>2016-12-01 23:23:28 +0100
committerJoram Wilander <jwawilander@gmail.com>2016-12-01 17:23:28 -0500
commitc51afba71a8d4614f74709d5e9c432c2cff3fcf7 (patch)
tree8b2ad4586123c5a7bab8c44f91dd8eebbfaea674
parent8c18da21f3e51421a0dc6fbd4be1fa1e838dd482 (diff)
downloadchat-c51afba71a8d4614f74709d5e9c432c2cff3fcf7.tar.gz
chat-c51afba71a8d4614f74709d5e9c432c2cff3fcf7.tar.bz2
chat-c51afba71a8d4614f74709d5e9c432c2cff3fcf7.zip
Add Team Description to the Team Settings (#4652)
* draft * Add Team Description to the Team Settings * add tooltips for team description * made changes per PM review * add message when there is no description set in the team * squash
-rw-r--r--api/team.go2
-rw-r--r--api/team_test.go40
-rw-r--r--i18n/en.json4
-rw-r--r--model/team.go5
-rw-r--r--store/sql_team_store.go2
-rw-r--r--store/sql_upgrade.go3
-rw-r--r--webapp/components/create_team/components/display_name.jsx2
-rw-r--r--webapp/components/select_team/components/select_team_item.jsx24
-rw-r--r--webapp/components/sidebar.jsx1
-rw-r--r--webapp/components/sidebar_header.jsx31
-rw-r--r--webapp/components/team_general_tab.jsx126
-rw-r--r--webapp/i18n/en.json5
-rw-r--r--webapp/sass/routes/_signup.scss12
-rw-r--r--webapp/tests/client_team.test.jsx18
-rw-r--r--webapp/utils/constants.jsx1
15 files changed, 264 insertions, 12 deletions
diff --git a/api/team.go b/api/team.go
index 8cfb4fe77..8abb66e59 100644
--- a/api/team.go
+++ b/api/team.go
@@ -775,6 +775,7 @@ func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) {
}
oldTeam.DisplayName = team.DisplayName
+ oldTeam.Description = team.Description
oldTeam.InviteId = team.InviteId
oldTeam.AllowOpenInvite = team.AllowOpenInvite
oldTeam.CompanyName = team.CompanyName
@@ -1010,6 +1011,7 @@ func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) {
result := map[string]string{}
result["display_name"] = team.DisplayName
+ result["description"] = team.Description
result["name"] = team.Name
result["id"] = team.Id
w.Write([]byte(model.MapToJson(result)))
diff --git a/api/team_test.go b/api/team_test.go
index ec3c40e51..910b58041 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -759,3 +759,43 @@ func TestGetTeamStats(t *testing.T) {
t.Fatal("should have errored - not on team")
}
}
+
+func TestUpdateTeamDescription(t *testing.T) {
+ th := Setup().InitBasic()
+ th.BasicClient.Logout()
+ Client := th.BasicClient
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "success+" + model.NewId() + "@simulator.amazonses.com", Type: model.TEAM_OPEN}
+ team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ user := &model.User{Email: team.Email, Nickname: "My Testing", Password: "passwd1"}
+ user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
+ LinkUserToTeam(user, team)
+ store.Must(Srv.Store.User().VerifyEmail(user.Id))
+
+ user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Jabba the Hutt", Password: "passwd1"}
+ user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
+ LinkUserToTeam(user2, team)
+ store.Must(Srv.Store.User().VerifyEmail(user2.Id))
+
+ Client.Login(user2.Email, "passwd1")
+ Client.SetTeamId(team.Id)
+
+ vteam := &model.Team{DisplayName: team.DisplayName, Name: team.Name, Description: team.Description, Email: team.Email, Type: team.Type}
+ vteam.Description = "yommamma"
+ if _, err := Client.UpdateTeam(vteam); err == nil {
+ t.Fatal("Should have errored, not admin")
+ }
+
+ Client.Login(user.Email, "passwd1")
+
+ vteam.Description = ""
+ if _, err := Client.UpdateTeam(vteam); err != nil {
+ t.Fatal("Should have errored, should save blank Description")
+ }
+
+ vteam.Description = "yommamma"
+ if _, err := Client.UpdateTeam(vteam); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/i18n/en.json b/i18n/en.json
index 97bae3b1a..9981b29b9 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -3772,6 +3772,10 @@
"translation": "Invalid name"
},
{
+ "id": "model.team.is_valid.description.app_error",
+ "translation": "Invalid description"
+ },
+ {
"id": "model.team.is_valid.reserved.app_error",
"translation": "This URL is unavailable. Please try another."
},
diff --git a/model/team.go b/model/team.go
index d54a809f4..3f05ce83a 100644
--- a/model/team.go
+++ b/model/team.go
@@ -24,6 +24,7 @@ type Team struct {
DeleteAt int64 `json:"delete_at"`
DisplayName string `json:"display_name"`
Name string `json:"name"`
+ Description string `json:"description"`
Email string `json:"email"`
Type string `json:"type"`
CompanyName string `json:"company_name"`
@@ -130,6 +131,10 @@ func (o *Team) IsValid() *AppError {
return NewLocAppError("Team.IsValid", "model.team.is_valid.url.app_error", nil, "id="+o.Id)
}
+ if len(o.Description) > 255 {
+ return NewLocAppError("Team.IsValid", "model.team.is_valid.description.app_error", nil, "id="+o.Id)
+ }
+
if IsReservedTeamName(o.Name) {
return NewLocAppError("Team.IsValid", "model.team.is_valid.reserved.app_error", nil, "id="+o.Id)
}
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 00f1f5c61..3ec336be5 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -27,6 +27,7 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore {
table.ColMap("Id").SetMaxSize(26)
table.ColMap("DisplayName").SetMaxSize(64)
table.ColMap("Name").SetMaxSize(64).SetUnique(true)
+ table.ColMap("Description").SetMaxSize(255)
table.ColMap("Email").SetMaxSize(128)
table.ColMap("CompanyName").SetMaxSize(64)
table.ColMap("AllowedDomains").SetMaxSize(500)
@@ -43,6 +44,7 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore {
func (s SqlTeamStore) CreateIndexesIfNotExists() {
s.CreateIndexIfNotExists("idx_teams_name", "Teams", "Name")
+ s.CreateIndexIfNotExists("idx_teams_description", "Teams", "Description")
s.CreateIndexIfNotExists("idx_teams_invite_id", "Teams", "InviteId")
s.CreateIndexIfNotExists("idx_teams_update_at", "Teams", "UpdateAt")
s.CreateIndexIfNotExists("idx_teams_create_at", "Teams", "CreateAt")
diff --git a/store/sql_upgrade.go b/store/sql_upgrade.go
index 38aac4299..8fb1da39c 100644
--- a/store/sql_upgrade.go
+++ b/store/sql_upgrade.go
@@ -218,6 +218,9 @@ func UpgradeDatabaseToVersion36(sqlStore *SqlStore) {
sqlStore.CreateColumnIfNotExists("Posts", "HasReactions", "tinyint", "boolean", "0")
+ // Create Team Description column
+ sqlStore.CreateColumnIfNotExists("Teams", "Description", "varchar(255)", "varchar(255)", "")
+
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
// TODO FIXME UNCOMMENT WHEN WE DO RELEASE
// !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
diff --git a/webapp/components/create_team/components/display_name.jsx b/webapp/components/create_team/components/display_name.jsx
index a557a48c5..67805a040 100644
--- a/webapp/components/create_team/components/display_name.jsx
+++ b/webapp/components/create_team/components/display_name.jsx
@@ -38,7 +38,7 @@ export default class TeamSignupDisplayNamePage extends React.Component {
this.setState({nameError: (
<FormattedMessage
id='create_team.display_name.charLength'
- defaultMessage='Name must be {min} or more characters up to a maximum of {max}'
+ defaultMessage='Name must be {min} or more characters up to a maximum of {max}. You can add a longer team description later.'
values={{
min: Constants.MIN_TEAMNAME_LENGTH,
max: Constants.MAX_TEAMNAME_LENGTH
diff --git a/webapp/components/select_team/components/select_team_item.jsx b/webapp/components/select_team/components/select_team_item.jsx
index b29c4b1c4..825afdd69 100644
--- a/webapp/components/select_team/components/select_team_item.jsx
+++ b/webapp/components/select_team/components/select_team_item.jsx
@@ -4,6 +4,7 @@
import React from 'react';
import {Link} from 'react-router/es6';
+import {Tooltip, OverlayTrigger} from 'react-bootstrap';
export default class SelectTeamItem extends React.Component {
static propTypes = {
@@ -35,8 +36,31 @@ export default class SelectTeamItem extends React.Component {
);
}
+ var descriptionTooltip = '';
+ var showDescriptionTooltip = '';
+ if (this.props.team.description) {
+ descriptionTooltip = (
+ <Tooltip id='team-description__tooltip'>
+ {this.props.team.description}
+ </Tooltip>
+ );
+
+ showDescriptionTooltip = (
+ <OverlayTrigger
+ trigger={['hover', 'focus', 'click']}
+ delayShow={1000}
+ placement='left'
+ overlay={descriptionTooltip}
+ ref='descriptionOverlay'
+ >
+ <span className='fa fa-info-circle signup-team__icon'/>
+ </OverlayTrigger>
+ );
+ }
+
return (
<div className='signup-team-dir'>
+ {showDescriptionTooltip}
<Link
to={this.props.url}
onClick={this.handleTeamClick}
diff --git a/webapp/components/sidebar.jsx b/webapp/components/sidebar.jsx
index 5c6645833..85d39c9e7 100644
--- a/webapp/components/sidebar.jsx
+++ b/webapp/components/sidebar.jsx
@@ -736,6 +736,7 @@ export default class Sidebar extends React.Component {
<SidebarHeader
teamDisplayName={this.state.currentTeam.display_name}
+ teamDescription={this.state.currentTeam.description}
teamName={this.state.currentTeam.name}
teamType={this.state.currentTeam.type}
currentUser={this.state.currentUser}
diff --git a/webapp/components/sidebar_header.jsx b/webapp/components/sidebar_header.jsx
index cd41cfb84..a5fbd2659 100644
--- a/webapp/components/sidebar_header.jsx
+++ b/webapp/components/sidebar_header.jsx
@@ -69,6 +69,25 @@ export default class SidebarHeader extends React.Component {
tutorialTip = createMenuTip(this.toggleDropdown);
}
+ let teamNameWithToolTip = null;
+ if (this.props.teamDescription === '') {
+ teamNameWithToolTip = (
+ <div className='team__name'>{this.props.teamDisplayName}</div>
+ );
+ } else {
+ teamNameWithToolTip = (
+ <OverlayTrigger
+ trigger={['hover', 'focus']}
+ delayShow={1000}
+ placement='bottom'
+ overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDescription}</Tooltip>}
+ ref='descriptionOverlay'
+ >
+ <div className='team__name'>{this.props.teamDisplayName}</div>
+ </OverlayTrigger>
+ );
+ }
+
return (
<div className='team__header theme'>
{tutorialTip}
@@ -79,15 +98,7 @@ export default class SidebarHeader extends React.Component {
{profilePicture}
<div className='header__info'>
<div className='user__name'>{'@' + me.username}</div>
- <OverlayTrigger
- trigger={['hover', 'focus']}
- delayShow={1000}
- placement='bottom'
- overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDisplayName}</Tooltip>}
- ref='descriptionOverlay'
- >
- <div className='team__name'>{this.props.teamDisplayName}</div>
- </OverlayTrigger>
+ {teamNameWithToolTip}
</div>
</a>
<SidebarHeaderDropdown
@@ -104,10 +115,12 @@ export default class SidebarHeader extends React.Component {
SidebarHeader.defaultProps = {
teamDisplayName: '',
+ teamDescription: '',
teamType: ''
};
SidebarHeader.propTypes = {
teamDisplayName: React.PropTypes.string,
+ teamDescription: React.PropTypes.string,
teamName: React.PropTypes.string,
teamType: React.PropTypes.string,
currentUser: React.PropTypes.object
diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx
index a5281d238..955a71ac5 100644
--- a/webapp/components/team_general_tab.jsx
+++ b/webapp/components/team_general_tab.jsx
@@ -55,6 +55,10 @@ const holders = defineMessages({
teamNameInfo: {
id: 'general_tab.teamNameInfo',
defaultMessage: 'Set the name of the team as it appears on your sign-in screen and at the top of the left-hand sidebar.'
+ },
+ teamDescriptionInfo: {
+ id: 'general_tab.teamDescriptionInfo',
+ defaultMessage: 'Team description provides additional information to help users select the right team. Maximum of 50 characters.'
}
});
@@ -68,9 +72,12 @@ class GeneralTab extends React.Component {
this.handleNameSubmit = this.handleNameSubmit.bind(this);
this.handleInviteIdSubmit = this.handleInviteIdSubmit.bind(this);
this.handleOpenInviteSubmit = this.handleOpenInviteSubmit.bind(this);
+ this.handleDescriptionSubmit = this.handleDescriptionSubmit.bind(this);
this.handleClose = this.handleClose.bind(this);
this.onUpdateNameSection = this.onUpdateNameSection.bind(this);
this.updateName = this.updateName.bind(this);
+ this.updateDescription = this.updateDescription.bind(this);
+ this.onUpdateDescriptionSection = this.onUpdateDescriptionSection.bind(this);
this.onUpdateInviteIdSection = this.onUpdateInviteIdSection.bind(this);
this.updateInviteId = this.updateInviteId.bind(this);
this.onUpdateOpenInviteSection = this.onUpdateOpenInviteSection.bind(this);
@@ -95,6 +102,7 @@ class GeneralTab extends React.Component {
name: team.display_name,
invite_id: team.invite_id,
allow_open_invite: team.allow_open_invite,
+ description: team.description,
serverError: '',
clientError: ''
};
@@ -103,6 +111,7 @@ class GeneralTab extends React.Component {
componentWillReceiveProps(nextProps) {
this.setState({
name: nextProps.team.display_name,
+ description: nextProps.team.description,
invite_id: nextProps.team.invite_id,
allow_open_invite: nextProps.team.allow_open_invite
});
@@ -215,6 +224,40 @@ class GeneralTab extends React.Component {
this.updateSection('');
}
+ handleDescriptionSubmit(e) {
+ e.preventDefault();
+
+ var state = {serverError: '', clientError: ''};
+ let valid = true;
+
+ const {formatMessage} = this.props.intl;
+ const description = this.state.description.trim();
+ if (description === this.props.team.description) {
+ state.clientError = formatMessage(holders.chooseName);
+ valid = false;
+ } else {
+ state.clientError = '';
+ }
+
+ this.setState(state);
+
+ if (!valid) {
+ return;
+ }
+
+ var data = this.props.team;
+ data.description = this.state.description;
+ updateTeam(data,
+ () => {
+ this.updateSection('');
+ },
+ (err) => {
+ state.serverError = err.message;
+ this.setState(state);
+ }
+ );
+ }
+
componentDidMount() {
$('#team_settings').on('hidden.bs.modal', this.handleClose);
}
@@ -232,6 +275,15 @@ class GeneralTab extends React.Component {
}
}
+ onUpdateDescriptionSection(e) {
+ e.preventDefault();
+ if (this.props.activeSection === 'description') {
+ this.updateSection('');
+ } else {
+ this.updateSection('description');
+ }
+ }
+
onUpdateInviteIdSection(e) {
e.preventDefault();
if (this.props.activeSection === 'invite_id') {
@@ -254,6 +306,10 @@ class GeneralTab extends React.Component {
this.setState({name: e.target.value});
}
+ updateDescription(e) {
+ this.setState({description: e.target.value});
+ }
+
updateInviteId(e) {
this.setState({invite_id: e.target.value});
}
@@ -457,6 +513,74 @@ class GeneralTab extends React.Component {
);
}
+ let descriptionSection;
+
+ if (this.props.activeSection === 'description') {
+ const inputs = [];
+
+ let teamDescriptionLabel = (
+ <FormattedMessage
+ id='general_tab.teamDescription'
+ defaultMessage='Team Description'
+ />
+ );
+ if (Utils.isMobile()) {
+ teamDescriptionLabel = '';
+ }
+
+ inputs.push(
+ <div
+ key='teamDescriptionSetting'
+ className='form-group'
+ >
+ <label className='col-sm-5 control-label'>{teamDescriptionLabel}</label>
+ <div className='col-sm-7'>
+ <input
+ className='form-control'
+ type='text'
+ maxLength={Constants.MAX_TEAMDESCRIPTION_LENGTH.toString()}
+ onChange={this.updateDescription}
+ value={this.state.description}
+ />
+ </div>
+ </div>
+ );
+
+ const descriptionExtraInfo = <span>{formatMessage(holders.teamDescriptionInfo)}</span>;
+
+ descriptionSection = (
+ <SettingItemMax
+ title={formatMessage({id: 'general_tab.teamDescription'})}
+ inputs={inputs}
+ submit={this.handleDescriptionSubmit}
+ server_error={serverError}
+ client_error={clientError}
+ updateSection={this.onUpdateDescriptionSection}
+ extraInfo={descriptionExtraInfo}
+ />
+ );
+ } else {
+ let describemsg = '';
+ if (this.state.description) {
+ describemsg = this.state.description;
+ } else {
+ describemsg = (
+ <FormattedMessage
+ id='general_tab.emptyDescription'
+ defaultMessage="Click 'Edit' to add a team description."
+ />
+ );
+ }
+
+ descriptionSection = (
+ <SettingItemMin
+ title={formatMessage({id: 'general_tab.teamDescription'})}
+ describe={describemsg}
+ updateSection={this.onUpdateDescriptionSection}
+ />
+ );
+ }
+
return (
<div>
<div className='modal-header'>
@@ -496,6 +620,8 @@ class GeneralTab extends React.Component {
<div className='divider-dark first'/>
{nameSection}
<div className='divider-light'/>
+ {descriptionSection}
+ <div className='divider-light'/>
{openInviteSection}
<div className='divider-light'/>
{inviteSection}
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index 3b87d76c2..1334c7358 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -1145,7 +1145,7 @@
"create_post.write": "Write a message...",
"create_team.agreement": "By proceeding to create your account and use {siteName}, you agree to our <a href={TermsOfServiceLink}>Terms of Service</a> and <a href={PrivacyPolicyLink}>Privacy Policy</a>. If you do not agree, you cannot use {siteName}.",
"create_team.display_name.back": "Back to previous step",
- "create_team.display_name.charLength": "Name must be 2 or more characters up to a maximum of 15",
+ "create_team.display_name.charLength": "Name must be {min} or more characters up to a maximum of {max}. You can add a longer team description later.",
"create_team.display_name.nameHelp": "Name your team in any language. Your team name shows in menus and headings.",
"create_team.display_name.next": "Next",
"create_team.display_name.required": "This field is required",
@@ -1272,8 +1272,11 @@
"general_tab.required": "This field is required",
"general_tab.teamName": "Team Name",
"general_tab.teamNameInfo": "Set the name of the team as it appears on your sign-in screen and at the top of the left-hand sidebar.",
+ "general_tab.teamDescription": "Team Description",
+ "general_tab.teamDescriptionInfo": "Team description provides additional information to help users select the right team. Maximum of 50 characters.",
"general_tab.title": "General Settings",
"general_tab.yes": "Yes",
+ "general_tab.emptyDescription": "Click 'Edit' to add a team description.",
"get_app.alreadyHaveIt": "Already have it?",
"get_app.androidAppName": "Mattermost for Android",
"get_app.androidHeader": "Mattermost works best if you switch to our Android app",
diff --git a/webapp/sass/routes/_signup.scss b/webapp/sass/routes/_signup.scss
index 30e80cccb..cbf6f1571 100644
--- a/webapp/sass/routes/_signup.scss
+++ b/webapp/sass/routes/_signup.scss
@@ -481,7 +481,8 @@
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
- width: 90%;
+ width: calc(100% - 50px);
+
}
.signup-team__icon {
@@ -497,6 +498,15 @@
right: -2px;
top: 16px;
}
+
+ &.fa-info-circle {
+ float: left;
+ line-height: 1.5em;
+ margin-right: .3em;
+ padding-left: .5em;
+ font-size: 1.5em;
+ top: 11px;
+ }
}
}
diff --git a/webapp/tests/client_team.test.jsx b/webapp/tests/client_team.test.jsx
index ab2e625bf..13b2802d2 100644
--- a/webapp/tests/client_team.test.jsx
+++ b/webapp/tests/client_team.test.jsx
@@ -237,6 +237,24 @@ describe('Client.Team', function() {
});
});
+ it('updateTeamDescription', function(done) {
+ TestHelper.initBasic(() => {
+ var team = TestHelper.basicTeam();
+ team.description = 'test_updated';
+
+ TestHelper.basicClient().updateTeam(
+ team,
+ function(data) {
+ assert.equal(data.description, 'test_updated');
+ done();
+ },
+ function(err) {
+ done(new Error(err.message));
+ }
+ );
+ });
+ });
+
it('addUserToTeam', function(done) {
TestHelper.initBasic(() => {
TestHelper.basicClient().createUser(
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index f94461ec7..2c881e024 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -829,6 +829,7 @@ export const Constants = {
DEFAULT_MAX_CHANNELS_PER_TEAM: 2000,
DEFAULT_MAX_NOTIFICATIONS_PER_CHANNEL: 1000,
MAX_TEAMNAME_LENGTH: 15,
+ MAX_TEAMDESCRIPTION_LENGTH: 50,
MIN_USERNAME_LENGTH: 3,
MAX_USERNAME_LENGTH: 22,
MAX_NICKNAME_LENGTH: 22,