From c51afba71a8d4614f74709d5e9c432c2cff3fcf7 Mon Sep 17 00:00:00 2001 From: Carlos Tadeu Panato Junior Date: Thu, 1 Dec 2016 23:23:28 +0100 Subject: 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 --- api/team.go | 2 + api/team_test.go | 40 +++++++ i18n/en.json | 4 + model/team.go | 5 + store/sql_team_store.go | 2 + store/sql_upgrade.go | 3 + .../create_team/components/display_name.jsx | 2 +- .../select_team/components/select_team_item.jsx | 24 ++++ webapp/components/sidebar.jsx | 1 + webapp/components/sidebar_header.jsx | 31 +++-- webapp/components/team_general_tab.jsx | 126 +++++++++++++++++++++ webapp/i18n/en.json | 5 +- webapp/sass/routes/_signup.scss | 12 +- webapp/tests/client_team.test.jsx | 18 +++ webapp/utils/constants.jsx | 1 + 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 @@ -3771,6 +3771,10 @@ "id": "model.team.is_valid.name.app_error", "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: ( + {this.props.team.description} + + ); + + showDescriptionTooltip = ( + + + + ); + } + return (
+ {showDescriptionTooltip} {this.props.teamDisplayName}
+ ); + } else { + teamNameWithToolTip = ( + {this.props.teamDescription}} + ref='descriptionOverlay' + > +
{this.props.teamDisplayName}
+
+ ); + } + return (
{tutorialTip} @@ -79,15 +98,7 @@ export default class SidebarHeader extends React.Component { {profilePicture}
{'@' + me.username}
- {this.props.teamDisplayName}} - ref='descriptionOverlay' - > -
{this.props.teamDisplayName}
-
+ {teamNameWithToolTip}
{ + 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 = ( + + ); + if (Utils.isMobile()) { + teamDescriptionLabel = ''; + } + + inputs.push( +
+ +
+ +
+
+ ); + + const descriptionExtraInfo = {formatMessage(holders.teamDescriptionInfo)}; + + descriptionSection = ( + + ); + } else { + let describemsg = ''; + if (this.state.description) { + describemsg = this.state.description; + } else { + describemsg = ( + + ); + } + + descriptionSection = ( + + ); + } + return (
@@ -496,6 +620,8 @@ class GeneralTab extends React.Component {
{nameSection}
+ {descriptionSection} +
{openInviteSection}
{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 Terms of Service and Privacy Policy. 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, -- cgit v1.2.3-1-g7c22