diff options
28 files changed, 757 insertions, 139 deletions
diff --git a/api/team.go b/api/team.go index d39d8ed60..7d746d922 100644 --- a/api/team.go +++ b/api/team.go @@ -30,7 +30,7 @@ func InitTeam(r *mux.Router) { sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST") sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST") sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST") - sr.Handle("/update_name", ApiUserRequired(updateTeamDisplayName)).Methods("POST") + sr.Handle("/update", ApiUserRequired(updateTeam)).Methods("POST") sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET") // These should be moved to the global admain console sr.Handle("/import_team", ApiUserRequired(importTeam)).Methods("POST") @@ -541,40 +541,47 @@ func InviteMembers(c *Context, team *model.Team, user *model.User, invites []str } } -func updateTeamDisplayName(c *Context, w http.ResponseWriter, r *http.Request) { +func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) { - props := model.MapFromJson(r.Body) + team := model.TeamFromJson(r.Body) - new_name := props["new_name"] - if len(new_name) == 0 { - c.SetInvalidParam("updateTeamDisplayName", "new_name") + if team == nil { + c.SetInvalidParam("updateTeam", "team") return } - teamId := props["team_id"] - if len(teamId) > 0 && len(teamId) != 26 { - c.SetInvalidParam("updateTeamDisplayName", "team_id") - return - } else if len(teamId) == 0 { - teamId = c.Session.TeamId - } + team.Id = c.Session.TeamId - if !c.HasPermissionsToTeam(teamId, "updateTeamDisplayName") { + if !c.IsTeamAdmin() { + c.Err = model.NewAppError("updateTeam", "You do not have the appropriate permissions", "userId="+c.Session.UserId) + c.Err.StatusCode = http.StatusForbidden return } - if !c.IsTeamAdmin() { - c.Err = model.NewAppError("updateTeamDisplayName", "You do not have the appropriate permissions", "userId="+c.Session.UserId) - c.Err.StatusCode = http.StatusForbidden + var oldTeam *model.Team + if result := <-Srv.Store.Team().Get(team.Id); result.Err != nil { + c.Err = result.Err return + } else { + oldTeam = result.Data.(*model.Team) } - if result := <-Srv.Store.Team().UpdateDisplayName(new_name, c.Session.TeamId); result.Err != nil { + oldTeam.DisplayName = team.DisplayName + oldTeam.InviteId = team.InviteId + oldTeam.AllowOpenInvite = team.AllowOpenInvite + oldTeam.AllowTeamListing = team.AllowTeamListing + oldTeam.CompanyName = team.CompanyName + oldTeam.AllowedDomains = team.AllowedDomains + //oldTeam.Type = team.Type + + if result := <-Srv.Store.Team().Update(oldTeam); result.Err != nil { c.Err = result.Err return } - w.Write([]byte(model.MapToJson(props))) + oldTeam.Sanitize() + + w.Write([]byte(oldTeam.ToJson())) } func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api/team_test.go b/api/team_test.go index 507f4252a..7a3b092ce 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -281,41 +281,23 @@ func TestUpdateTeamDisplayName(t *testing.T) { Client.LoginByEmail(team.Name, user2.Email, "pwd") - data := make(map[string]string) - data["new_name"] = "NewName" - if _, err := Client.UpdateTeamDisplayName(data); err == nil { + vteam := &model.Team{DisplayName: team.DisplayName, Name: team.Name, Email: team.Email, Type: team.Type} + vteam.DisplayName = "NewName" + if _, err := Client.UpdateTeam(vteam); err == nil { t.Fatal("Should have errored, not admin") } Client.LoginByEmail(team.Name, user.Email, "pwd") - data["new_name"] = "" - if _, err := Client.UpdateTeamDisplayName(data); err == nil { + vteam.DisplayName = "" + if _, err := Client.UpdateTeam(vteam); err == nil { t.Fatal("Should have errored, empty name") } - data["new_name"] = "NewName" - if _, err := Client.UpdateTeamDisplayName(data); err != nil { + vteam.DisplayName = "NewName" + if _, err := Client.UpdateTeam(vteam); err != nil { t.Fatal(err) } - // No GET team web service, so hard to confirm here that team name updated - - data["team_id"] = "junk" - if _, err := Client.UpdateTeamDisplayName(data); err == nil { - t.Fatal("Should have errored, junk team id") - } - - data["team_id"] = "12345678901234567890123456" - if _, err := Client.UpdateTeamDisplayName(data); err == nil { - t.Fatal("Should have errored, bad team id") - } - - data["team_id"] = team.Id - data["new_name"] = "NewNameAgain" - if _, err := Client.UpdateTeamDisplayName(data); err != nil { - t.Fatal(err) - } - // No GET team web service, so hard to confirm here that team name updated } func TestFuzzyTeamCreate(t *testing.T) { diff --git a/api/user_test.go b/api/user_test.go index b54e030c5..0ad3541bc 100644 --- a/api/user_test.go +++ b/api/user_test.go @@ -661,12 +661,6 @@ func TestUserUpdateRoles(t *testing.T) { t.Fatal("Should have errored, not admin") } - name := make(map[string]string) - name["new_name"] = "NewName" - if _, err := Client.UpdateTeamDisplayName(name); err == nil { - t.Fatal("should have errored - user not admin yet") - } - team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) @@ -707,12 +701,6 @@ func TestUserUpdateRoles(t *testing.T) { t.Fatal("Roles did not update properly") } } - - Client.LoginByEmail(team.Name, user2.Email, "pwd") - - if _, err := Client.UpdateTeamDisplayName(name); err != nil { - t.Fatal(err) - } } func TestUserUpdateActive(t *testing.T) { diff --git a/config/config.json b/config/config.json index 7bac58df7..a927620b5 100644 --- a/config/config.json +++ b/config/config.json @@ -18,7 +18,8 @@ "EnableTeamCreation": true, "EnableUserCreation": true, "RestrictCreationToDomains": "", - "RestrictTeamNames": true + "RestrictTeamNames": true, + "EnableTeamListing": false }, "SqlSettings": { "DriverName": "mysql", diff --git a/docker/dev/config_docker.json b/docker/dev/config_docker.json index 00729395e..80e6ab14e 100644 --- a/docker/dev/config_docker.json +++ b/docker/dev/config_docker.json @@ -17,7 +17,9 @@ "MaxUsersPerTeam": 50, "EnableTeamCreation": true, "EnableUserCreation": true, - "RestrictCreationToDomains": "" + "RestrictCreationToDomains": "", + "RestrictTeamNames": true, + "EnableTeamListing": false }, "SqlSettings": { "DriverName": "mysql", diff --git a/docker/local/config_docker.json b/docker/local/config_docker.json index 00729395e..80e6ab14e 100644 --- a/docker/local/config_docker.json +++ b/docker/local/config_docker.json @@ -17,7 +17,9 @@ "MaxUsersPerTeam": 50, "EnableTeamCreation": true, "EnableUserCreation": true, - "RestrictCreationToDomains": "" + "RestrictCreationToDomains": "", + "RestrictTeamNames": true, + "EnableTeamListing": false }, "SqlSettings": { "DriverName": "mysql", diff --git a/model/client.go b/model/client.go index 4d2c49e70..9232bac5b 100644 --- a/model/client.go +++ b/model/client.go @@ -211,8 +211,8 @@ func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) { } } -func (c *Client) UpdateTeamDisplayName(data map[string]string) (*Result, *AppError) { - if r, err := c.DoApiPost("/teams/update_name", MapToJson(data)); err != nil { +func (c *Client) UpdateTeam(team *Team) (*Result, *AppError) { + if r, err := c.DoApiPost("/teams/update", team.ToJson()); err != nil { return nil, err } else { return &Result{r.Header.Get(HEADER_REQUEST_ID), diff --git a/model/config.go b/model/config.go index 216b1de86..50a8dc133 100644 --- a/model/config.go +++ b/model/config.go @@ -123,6 +123,7 @@ type TeamSettings struct { EnableUserCreation bool RestrictCreationToDomains string RestrictTeamNames *bool + EnableTeamListing *bool } type Config struct { @@ -175,6 +176,11 @@ func (o *Config) SetDefaults() { o.TeamSettings.RestrictTeamNames = new(bool) *o.TeamSettings.RestrictTeamNames = true } + + if o.TeamSettings.EnableTeamListing == nil { + o.TeamSettings.EnableTeamListing = new(bool) + *o.TeamSettings.EnableTeamListing = false + } } func (o *Config) IsValid() *AppError { diff --git a/model/team.go b/model/team.go index 7f301b71c..4d14ec2ee 100644 --- a/model/team.go +++ b/model/team.go @@ -122,7 +122,7 @@ func (o *Team) IsValid(restrictTeamNames bool) *AppError { return NewAppError("Team.IsValid", "Invalid email", "id="+o.Id) } - if len(o.DisplayName) > 64 { + if len(o.DisplayName) == 0 || len(o.DisplayName) > 64 { return NewAppError("Team.IsValid", "Invalid name", "id="+o.Id) } @@ -150,10 +150,6 @@ func (o *Team) IsValid(restrictTeamNames bool) *AppError { return NewAppError("Team.IsValid", "Invalid allowed domains", "id="+o.Id) } - if len(o.InviteId) > 0 && len(o.InviteId) != 26 { - return NewAppError("Team.IsValid", "Invalid inviate Id", "") - } - return nil } @@ -164,6 +160,10 @@ func (o *Team) PreSave() { o.CreateAt = GetMillis() o.UpdateAt = o.CreateAt + + if len(o.InviteId) == 0 { + o.InviteId = NewId() + } } func (o *Team) PreUpdate() { diff --git a/store/sql_team_store.go b/store/sql_team_store.go index e5a25b80d..cacc8595e 100644 --- a/store/sql_team_store.go +++ b/store/sql_team_store.go @@ -23,6 +23,7 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore { table.ColMap("Email").SetMaxSize(128) table.ColMap("CompanyName").SetMaxSize(64) table.ColMap("AllowedDomains").SetMaxSize(500) + table.ColMap("InviteId").SetMaxSize(32) } return s @@ -38,6 +39,7 @@ func (s SqlTeamStore) UpgradeSchemaIfNeeded() { func (s SqlTeamStore) CreateIndexesIfNotExists() { s.CreateIndexIfNotExists("idx_teams_name", "Teams", "Name") + s.CreateIndexIfNotExists("idx_teams_invite_id", "Teams", "InviteId") } func (s SqlTeamStore) Save(team *model.Team) StoreChannel { @@ -101,6 +103,7 @@ func (s SqlTeamStore) Update(team *model.Team) StoreChannel { } else { oldTeam := oldResult.(*model.Team) team.CreateAt = oldTeam.CreateAt + team.UpdateAt = model.GetMillis() team.Name = oldTeam.Name if count, err := s.GetMaster().Update(team); err != nil { @@ -150,7 +153,12 @@ func (s SqlTeamStore) Get(id string) StoreChannel { } else if obj == nil { result.Err = model.NewAppError("SqlTeamStore.Get", "We couldn't find the existing team", "id="+id) } else { - result.Data = obj.(*model.Team) + team := obj.(*model.Team) + if len(team.InviteId) == 0 { + team.InviteId = team.Id + } + + result.Data = team } storeChannel <- result @@ -160,6 +168,35 @@ func (s SqlTeamStore) Get(id string) StoreChannel { return storeChannel } +func (s SqlTeamStore) GetByInviteId(inviteId string) StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + team := model.Team{} + + if err := s.GetReplica().SelectOne(&team, "SELECT * FROM Teams WHERE Id = :InviteId OR InviteId = :InviteId", map[string]interface{}{"InviteId": inviteId}); err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetByInviteId", "We couldn't find the existing team", "inviteId="+inviteId+", "+err.Error()) + } + + if len(team.InviteId) == 0 { + team.InviteId = team.Id + } + + if len(inviteId) == 0 || team.InviteId != inviteId { + result.Err = model.NewAppError("SqlTeamStore.GetByInviteId", "We couldn't find the existing team", "inviteId="+inviteId) + } + + result.Data = &team + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + func (s SqlTeamStore) GetByName(name string) StoreChannel { storeChannel := make(StoreChannel) @@ -172,6 +209,10 @@ func (s SqlTeamStore) GetByName(name string) StoreChannel { result.Err = model.NewAppError("SqlTeamStore.GetByName", "We couldn't find the existing team", "name="+name+", "+err.Error()) } + if len(team.InviteId) == 0 { + team.InviteId = team.Id + } + result.Data = &team storeChannel <- result @@ -192,6 +233,12 @@ func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel { result.Err = model.NewAppError("SqlTeamStore.GetTeamsForEmail", "We encounted a problem when looking up teams", "email="+email+", "+err.Error()) } + for _, team := range data { + if len(team.InviteId) == 0 { + team.InviteId = team.Id + } + } + result.Data = data storeChannel <- result @@ -212,6 +259,38 @@ func (s SqlTeamStore) GetAll() StoreChannel { result.Err = model.NewAppError("SqlTeamStore.GetAllTeams", "We could not get all teams", err.Error()) } + for _, team := range data { + if len(team.InviteId) == 0 { + team.InviteId = team.Id + } + } + + result.Data = data + + storeChannel <- result + close(storeChannel) + }() + + return storeChannel +} + +func (s SqlTeamStore) GetAllTeamListing() StoreChannel { + storeChannel := make(StoreChannel) + + go func() { + result := StoreResult{} + + var data []*model.Team + if _, err := s.GetReplica().Select(&data, "SELECT * FROM Teams WHERE AllowTeamListing = 1"); err != nil { + result.Err = model.NewAppError("SqlTeamStore.GetAllTeams", "We could not get all teams", err.Error()) + } + + for _, team := range data { + if len(team.InviteId) == 0 { + team.InviteId = team.Id + } + } + result.Data = data storeChannel <- result diff --git a/store/sql_team_store_test.go b/store/sql_team_store_test.go index 3d9b4d435..71740f7e7 100644 --- a/store/sql_team_store_test.go +++ b/store/sql_team_store_test.go @@ -132,6 +132,54 @@ func TestTeamStoreGetByName(t *testing.T) { } } +func TestTeamStoreGetByIniviteId(t *testing.T) { + Setup() + + o1 := model.Team{} + o1.DisplayName = "DisplayName" + o1.Name = "a" + model.NewId() + "b" + o1.Email = model.NewId() + "@nowhere.com" + o1.Type = model.TEAM_OPEN + o1.InviteId = model.NewId() + + if err := (<-store.Team().Save(&o1)).Err; err != nil { + t.Fatal(err) + } + + o2 := model.Team{} + o2.DisplayName = "DisplayName" + o2.Name = "a" + model.NewId() + "b" + o2.Email = model.NewId() + "@nowhere.com" + o2.Type = model.TEAM_OPEN + + if err := (<-store.Team().Save(&o2)).Err; err != nil { + t.Fatal(err) + } + + if r1 := <-store.Team().GetByInviteId(o1.InviteId); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.Team).ToJson() != o1.ToJson() { + t.Fatal("invalid returned team") + } + } + + o2.InviteId = "" + <-store.Team().Update(&o2) + + if r1 := <-store.Team().GetByInviteId(o2.Id); r1.Err != nil { + t.Fatal(r1.Err) + } else { + if r1.Data.(*model.Team).Id != o2.Id { + t.Fatal("invalid returned team") + } + } + + if err := (<-store.Team().GetByInviteId("")).Err; err == nil { + t.Fatal("Missing id should have failed") + } +} + func TestTeamStoreGetForEmail(t *testing.T) { Setup() @@ -161,3 +209,32 @@ func TestTeamStoreGetForEmail(t *testing.T) { t.Fatal(r1.Err) } } + +func TestAllTeamListing(t *testing.T) { + Setup() + + o1 := model.Team{} + o1.DisplayName = "DisplayName" + o1.Name = "a" + model.NewId() + "b" + o1.Email = model.NewId() + "@nowhere.com" + o1.Type = model.TEAM_OPEN + o1.AllowTeamListing = true + Must(store.Team().Save(&o1)) + + o2 := model.Team{} + o2.DisplayName = "DisplayName" + o2.Name = "a" + model.NewId() + "b" + o2.Email = model.NewId() + "@nowhere.com" + o2.Type = model.TEAM_OPEN + Must(store.Team().Save(&o2)) + + if r1 := <-store.Team().GetAllTeamListing(); r1.Err != nil { + t.Fatal(r1.Err) + } else { + teams := r1.Data.([]*model.Team) + + if len(teams) == 0 { + t.Fatal("failed team listing") + } + } +} diff --git a/store/store.go b/store/store.go index 42329b036..53a6e053b 100644 --- a/store/store.go +++ b/store/store.go @@ -50,6 +50,8 @@ type TeamStore interface { GetByName(name string) StoreChannel GetTeamsForEmail(domain string) StoreChannel GetAll() StoreChannel + GetAllTeamListing() StoreChannel + GetByInviteId(inviteId string) StoreChannel } type ChannelStore interface { diff --git a/utils/config.go b/utils/config.go index 6b34c76ed..13b7b6b64 100644 --- a/utils/config.go +++ b/utils/config.go @@ -190,6 +190,7 @@ func getClientConfig(c *model.Config) map[string]string { props["SiteName"] = c.TeamSettings.SiteName props["EnableTeamCreation"] = strconv.FormatBool(c.TeamSettings.EnableTeamCreation) props["RestrictTeamNames"] = strconv.FormatBool(*c.TeamSettings.RestrictTeamNames) + props["EnableTeamListing"] = strconv.FormatBool(*c.TeamSettings.EnableTeamListing) props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider) diff --git a/web/react/components/admin_console/team_settings.jsx b/web/react/components/admin_console/team_settings.jsx index 9ecd14a1e..6587184ea 100644 --- a/web/react/components/admin_console/team_settings.jsx +++ b/web/react/components/admin_console/team_settings.jsx @@ -32,6 +32,7 @@ export default class TeamSettings extends React.Component { config.TeamSettings.EnableTeamCreation = ReactDOM.findDOMNode(this.refs.EnableTeamCreation).checked; config.TeamSettings.EnableUserCreation = ReactDOM.findDOMNode(this.refs.EnableUserCreation).checked; config.TeamSettings.RestrictTeamNames = ReactDOM.findDOMNode(this.refs.RestrictTeamNames).checked; + config.TeamSettings.EnableTeamListing = ReactDOM.findDOMNode(this.refs.EnableTeamListing).checked; var MaxUsersPerTeam = 50; if (!isNaN(parseInt(ReactDOM.findDOMNode(this.refs.MaxUsersPerTeam).value, 10))) { @@ -243,6 +244,39 @@ export default class TeamSettings extends React.Component { </div> <div className='form-group'> + <label + className='control-label col-sm-4' + htmlFor='EnableTeamListing' + > + {'Enable Team Directory: '} + </label> + <div className='col-sm-8'> + <label className='radio-inline'> + <input + type='radio' + name='EnableTeamListing' + value='true' + ref='EnableTeamListing' + defaultChecked={this.props.config.TeamSettings.EnableTeamListing} + onChange={this.handleChange} + /> + {'true'} + </label> + <label className='radio-inline'> + <input + type='radio' + name='EnableTeamListing' + value='false' + defaultChecked={!this.props.config.TeamSettings.EnableTeamListing} + onChange={this.handleChange} + /> + {'false'} + </label> + <p className='help-text'>{'When true, teams that are configured to show in team directory will show on main page inplace of creating a new team.'}</p> + </div> + </div> + + <div className='form-group'> <div className='col-sm-12'> {serverError} <button diff --git a/web/react/components/invite_member_modal.jsx b/web/react/components/invite_member_modal.jsx index 86a4b04cf..bea700725 100644 --- a/web/react/components/invite_member_modal.jsx +++ b/web/react/components/invite_member_modal.jsx @@ -4,6 +4,7 @@ var utils = require('../utils/utils.jsx'); var Client = require('../utils/client.jsx'); var UserStore = require('../stores/user_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); var ConfirmModal = require('./confirm_modal.jsx'); export default class InviteMemberModal extends React.Component { @@ -292,7 +293,7 @@ export default class InviteMemberModal extends React.Component { } else { var teamInviteLink = null; if (currentUser && this.props.teamType === 'O') { - var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id; + var linkUrl = utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id; var link = ( <a diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 108735caf..c519959af 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -101,7 +101,7 @@ export default class Login extends React.Component { href={'/' + teamName + '/login/gitlab'} > <span className='icon' /> - <span>with GitLab</span> + <span>{'with GitLab'}</span> </a> ); } @@ -154,7 +154,7 @@ export default class Login extends React.Component { type='submit' className='btn btn-primary' > - Sign in + {'Sign in'} </button> </div> </div> @@ -166,7 +166,7 @@ export default class Login extends React.Component { <div> {loginMessage} <div className='or__container'> - <span>or</span> + <span>{'or'}</span> </div> </div> ); @@ -176,16 +176,48 @@ export default class Login extends React.Component { if (emailSignup) { forgotPassword = ( <div className='form-group'> - <a href={'/' + teamName + '/reset_password'}>I forgot my password</a> + <a href={'/' + teamName + '/reset_password'}>{'I forgot my password'}</a> + </div> + ); + } + + let userSignUp = null; + if (this.props.inviteId) { + userSignUp = ( + <div> + <span>{'Do not have an account? '} + <a + href={'/signup_user_complete/?id=' + this.props.inviteId} + className='signup-team-login' + > + {'Create one now'} + </a> + </span> + </div> + ); + } + + let teamSignUp = null; + if (global.window.mm_config.EnableTeamCreation === 'true') { + teamSignUp = ( + <div className='margin--extra'> + <span>{'Want to create your own team? '} + <a + href='/' + className='signup-team-login' + > + {'Sign up now'} + </a> + </span> </div> ); } return ( <div className='signup-team__container'> - <h5 className='margin--less'>Sign in to:</h5> + <h5 className='margin--less'>{'Sign in to:'}</h5> <h2 className='signup-team__name'>{teamDisplayName}</h2> - <h2 className='signup-team__subdomain'>on {global.window.mm_config.SiteName}</h2> + <h2 className='signup-team__subdomain'>{'on '}{global.window.mm_config.SiteName}</h2> <form onSubmit={this.handleSubmit}> {verifiedBox} <div className={'form-group' + errorClass}> @@ -193,20 +225,12 @@ export default class Login extends React.Component { </div> {loginMessage} {emailSignup} + {userSignUp} <div className='form-group margin--extra form-group--small'> <span><a href='/find_team'>{'Find other teams'}</a></span> </div> {forgotPassword} - <div className='margin--extra'> - <span>{'Want to create your own team? '} - <a - href='/' - className='signup-team-login' - > - Sign up now - </a> - </span> - </div> + {teamSignUp} </form> </div> ); @@ -219,5 +243,6 @@ Login.defaultProps = { }; Login.propTypes = { teamName: React.PropTypes.string, - teamDisplayName: React.PropTypes.string + teamDisplayName: React.PropTypes.string, + inviteId: React.PropTypes.string }; diff --git a/web/react/components/navbar_dropdown.jsx b/web/react/components/navbar_dropdown.jsx index 2b68645e5..cc041e094 100644 --- a/web/react/components/navbar_dropdown.jsx +++ b/web/react/components/navbar_dropdown.jsx @@ -111,7 +111,7 @@ export default class NavbarDropdown extends React.Component { data-toggle='modal' data-target='#get_link' data-title='Team Invite' - data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id} + data-value={Utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id} > {'Get Team Invite Link'} </a> diff --git a/web/react/components/sidebar_right_menu.jsx b/web/react/components/sidebar_right_menu.jsx index fddc98c9d..9350bbd42 100644 --- a/web/react/components/sidebar_right_menu.jsx +++ b/web/react/components/sidebar_right_menu.jsx @@ -2,6 +2,7 @@ // See License.txt for license information. var UserStore = require('../stores/user_store.jsx'); +var TeamStore = require('../stores/team_store.jsx'); var client = require('../utils/client.jsx'); var utils = require('../utils/utils.jsx'); @@ -51,7 +52,7 @@ export default class SidebarRightMenu extends React.Component { data-toggle='modal' data-target='#get_link' data-title='Team Invite' - data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + currentUser.team_id} + data-value={utils.getWindowLocationOrigin() + '/signup_user_complete/?id=' + TeamStore.getCurrent().invite_id} ><i className='glyphicon glyphicon-link'></i>Get Team Invite Link</a> </li> ); diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index 1858703ef..814ec2fc1 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -12,6 +12,11 @@ export default class TeamSignUp extends React.Component { this.updatePage = this.updatePage.bind(this); + if (global.window.mm_config.EnableTeamListing === 'true') { + this.state = {page: 'team_listing'}; + return; + } + var count = 0; if (global.window.mm_config.EnableSignUpWithEmail === 'true') { @@ -31,11 +36,49 @@ export default class TeamSignUp extends React.Component { } } + componentDidMount() { + if (global.window.mm_config.EnableTeamListing === 'true' && this.props.teams.length === 1) { + window.location.href = '/' + this.props.teams[0].name; + } + } + updatePage(page) { this.setState({page}); } render() { + if (this.state.page === 'team_listing') { + return ( + <div> + <h3>{'Choose a Team'}</h3> + <div className='signup-team-all'> + { + this.props.teams.map((team) => { + return ( + <div + key={'team_' + team.name} + className='signup-team-dir' + > + <a + href={'/' + team.name} + > + <div className='signup-team-dir__group'> + <span className='signup-team-dir__name'>{team.display_name}</span> + <span + className='glyphicon glyphicon-menu-right right signup-team-dir__arrow' + aria-hidden='true' + /> + </div> + </a> + </div> + ); + }) + } + </div> + </div> + ); + } + if (this.state.page === 'choose') { return ( <ChoosePage @@ -51,3 +94,8 @@ export default class TeamSignUp extends React.Component { } } } + +TeamSignUp.propTypes = { + teams: React.PropTypes.array +}; + diff --git a/web/react/components/team_general_tab.jsx b/web/react/components/team_general_tab.jsx index 923180e27..760c43965 100644 --- a/web/react/components/team_general_tab.jsx +++ b/web/react/components/team_general_tab.jsx @@ -6,29 +6,109 @@ const SettingItemMax = require('./setting_item_max.jsx'); const Client = require('../utils/client.jsx'); const Utils = require('../utils/utils.jsx'); +const TeamStore = require('../stores/team_store.jsx'); export default class GeneralTab extends React.Component { constructor(props) { super(props); this.handleNameSubmit = this.handleNameSubmit.bind(this); + this.handleInviteIdSubmit = this.handleInviteIdSubmit.bind(this); + this.handleOpenInviteSubmit = this.handleOpenInviteSubmit.bind(this); + this.handleTeamListingSubmit = this.handleTeamListingSubmit.bind(this); this.handleClose = this.handleClose.bind(this); - this.onUpdateSection = this.onUpdateSection.bind(this); + this.onUpdateNameSection = this.onUpdateNameSection.bind(this); this.updateName = this.updateName.bind(this); + this.onUpdateInviteIdSection = this.onUpdateInviteIdSection.bind(this); + this.updateInviteId = this.updateInviteId.bind(this); + this.onUpdateOpenInviteSection = this.onUpdateOpenInviteSection.bind(this); + this.handleOpenInviteRadio = this.handleOpenInviteRadio.bind(this); + this.onUpdateTeamListingSection = this.onUpdateTeamListingSection.bind(this); + this.handleTeamListingRadio = this.handleTeamListingRadio.bind(this); + this.handleGenerateInviteId = this.handleGenerateInviteId.bind(this); - this.state = {name: this.props.teamDisplayName, serverError: '', clientError: ''}; + this.state = { + name: props.team.display_name, + invite_id: props.team.invite_id, + allow_open_invite: props.team.allow_open_invite, + allow_team_listing: props.team.allow_team_listing, + serverError: '', + clientError: '' + }; } + + handleGenerateInviteId(e) { + e.preventDefault(); + + var newId = ''; + for (var i = 0; i < 32; i++) { + newId += Math.floor(Math.random() * 16).toString(16); + } + + console.log(newId); + + this.setState({invite_id: newId}); + } + + handleOpenInviteRadio(openInvite) { + this.setState({allow_open_invite: openInvite}); + } + + handleTeamListingRadio(listing) { + this.setState({allow_team_listing: listing}); + } + + handleOpenInviteSubmit(e) { + e.preventDefault(); + + var state = {serverError: '', clientError: ''}; + + var data = this.props.team; + data.allow_open_invite = this.state.allow_open_invite; + Client.updateTeam(data, + (team) => { + TeamStore.saveTeam(team); + TeamStore.emitChange(); + this.props.updateSection(''); + }, + (err) => { + state.serverError = err.message; + this.setState(state); + } + ); + } + + handleTeamListingSubmit(e) { + e.preventDefault(); + + var state = {serverError: '', clientError: ''}; + + var data = this.props.team; + data.allow_team_listing = this.state.allow_team_listing; + Client.updateTeam(data, + (team) => { + TeamStore.saveTeam(team); + TeamStore.emitChange(); + this.props.updateSection(''); + }, + (err) => { + state.serverError = err.message; + this.setState(state); + } + ); + } + handleNameSubmit(e) { e.preventDefault(); - let state = {serverError: '', clientError: ''}; + var state = {serverError: '', clientError: ''}; let valid = true; const name = this.state.name.trim(); if (!name) { state.clientError = 'This field is required'; valid = false; - } else if (name === this.props.teamDisplayName) { + } else if (name === this.props.team.display_name) { state.clientError = 'Please choose a new name for your team'; valid = false; } else { @@ -41,37 +121,76 @@ export default class GeneralTab extends React.Component { return; } - let data = {}; - data.new_name = name; + var data = this.props.team; + data.display_name = this.state.name; + Client.updateTeam(data, + (team) => { + TeamStore.saveTeam(team); + TeamStore.emitChange(); + this.props.updateSection(''); + }, + (err) => { + state.serverError = err.message; + this.setState(state); + } + ); + } + + handleInviteIdSubmit(e) { + e.preventDefault(); + + var state = {serverError: '', clientError: ''}; + let valid = true; + + const inviteId = this.state.invite_id.trim(); + if (inviteId) { + state.clientError = ''; + } else { + state.clientError = 'This field is required'; + valid = false; + } + + this.setState(state); - Client.updateTeamDisplayName(data, - function nameChangeSuccess() { + if (!valid) { + return; + } + + var data = this.props.team; + data.invite_id = this.state.invite_id; + Client.updateTeam(data, + (team) => { + TeamStore.saveTeam(team); + TeamStore.emitChange(); this.props.updateSection(''); - $('#team_settings').modal('hide'); - window.location.reload(); - }.bind(this), - function nameChangeFail(err) { + }, + (err) => { state.serverError = err.message; this.setState(state); - }.bind(this) + } ); } + componentWillReceiveProps(newProps) { if (newProps.team && newProps.teamDisplayName) { this.setState({name: newProps.teamDisplayName}); } } + handleClose() { this.setState({clientError: '', serverError: ''}); this.props.updateSection(''); } + componentDidMount() { $('#team_settings').on('hidden.bs.modal', this.handleClose); } + componentWillUnmount() { $('#team_settings').off('hidden.bs.modal', this.handleClose); } - onUpdateSection(e) { + + onUpdateNameSection(e) { e.preventDefault(); if (this.props.activeSection === 'name') { this.props.updateSection(''); @@ -79,10 +198,44 @@ export default class GeneralTab extends React.Component { this.props.updateSection('name'); } } + + onUpdateInviteIdSection(e) { + e.preventDefault(); + if (this.props.activeSection === 'invite_id') { + this.props.updateSection(''); + } else { + this.props.updateSection('invite_id'); + } + } + + onUpdateOpenInviteSection(e) { + e.preventDefault(); + if (this.props.activeSection === 'open_invite') { + this.props.updateSection(''); + } else { + this.props.updateSection('open_invite'); + } + } + + onUpdateTeamListingSection(e) { + e.preventDefault(); + if (this.props.activeSection === 'team_listing') { + this.props.updateSection(''); + } else { + this.props.updateSection('team_listing'); + } + } + updateName(e) { e.preventDefault(); this.setState({name: e.target.value}); } + + updateInviteId(e) { + e.preventDefault(); + this.setState({invite_id: e.target.value}); + } + render() { let clientError = null; let serverError = null; @@ -93,10 +246,178 @@ export default class GeneralTab extends React.Component { serverError = this.state.serverError; } + let teamListingSection; + if (this.props.activeSection === 'team_listing') { + const inputs = [ + <div key='userTeamListingOptions'> + <div className='radio'> + <label> + <input + name='userTeamListingOptions' + type='radio' + defaultChecked={this.state.allow_team_listing} + onChange={this.handleTeamListingRadio.bind(this, true)} + /> + {'Yes'} + </label> + <br/> + </div> + <div className='radio'> + <label> + <input + name='userTeamListingOptions' + type='radio' + defaultChecked={!this.state.allow_team_listing} + onChange={this.handleTeamListingRadio.bind(this, false)} + /> + {'No'} + </label> + <br/> + </div> + <div><br/>{'When allowed then the team will appear on the main page as part of team directory if team browsing is enabled in the system console.'}</div> + </div> + ]; + + teamListingSection = ( + <SettingItemMax + title='Allow in Team Directory' + inputs={inputs} + submit={this.handleTeamListingSubmit} + server_error={serverError} + updateSection={this.onUpdateTeamListingSection} + /> + ); + } else { + let describe = ''; + if (this.state.allow_team_listing === true) { + describe = 'Yes'; + } else { + describe = 'No'; + } + + teamListingSection = ( + <SettingItemMin + title='Allow in Team Directory' + describe={describe} + updateSection={this.onUpdateTeamListingSection} + /> + ); + } + + let openInviteSection; + if (this.props.activeSection === 'open_invite') { + const inputs = [ + <div key='userOpenInviteOptions'> + <div className='radio'> + <label> + <input + name='userOpenInviteOptions' + type='radio' + defaultChecked={this.state.allow_open_invite} + onChange={this.handleOpenInviteRadio.bind(this, true)} + /> + {'Yes'} + </label> + <br/> + </div> + <div className='radio'> + <label> + <input + name='userOpenInviteOptions' + type='radio' + defaultChecked={!this.state.allow_open_invite} + onChange={this.handleOpenInviteRadio.bind(this, false)} + /> + {'No'} + </label> + <br/> + </div> + <div><br/>{'When allowed the team signup link will be included on the login page and anyone can signup to this team.'}</div> + </div> + ]; + + openInviteSection = ( + <SettingItemMax + title='Allow Open Invitations' + inputs={inputs} + submit={this.handleOpenInviteSubmit} + server_error={serverError} + updateSection={this.onUpdateOpenInviteSection} + /> + ); + } else { + let describe = ''; + if (this.state.allow_open_invite === true) { + describe = 'Yes'; + } else { + describe = 'No'; + } + + openInviteSection = ( + <SettingItemMin + title='Allow Open Invitations' + describe={describe} + updateSection={this.onUpdateOpenInviteSection} + /> + ); + } + + let inviteSection; + + if (this.props.activeSection === 'invite_id') { + const inputs = []; + + inputs.push( + <div + key='teamInviteSetting' + className='form-group' + > + <label className='col-sm-5 control-label'>{'Invite Code'}</label> + <div className='col-sm-7'> + <input + className='form-control' + type='text' + onChange={this.updateInviteId} + value={this.state.invite_id} + maxLength='32' + /> + </div> + <div><br/>{'When allowing open invites this code is used as part of the signup process. Changing this code will invalidate the previous open signup link.'}</div> + <div className='help-text'> + <button + className='btn btn-default' + onClick={this.handleGenerateInviteId} + > + {'Re-Generate'} + </button> + </div> + </div> + ); + + inviteSection = ( + <SettingItemMax + title={`Invite Code`} + inputs={inputs} + submit={this.handleInviteIdSubmit} + server_error={serverError} + client_error={clientError} + updateSection={this.onUpdateInviteIdSection} + /> + ); + } else { + inviteSection = ( + <SettingItemMin + title={`Invite Code`} + describe={`Click 'Edit' to re-generate invite Code.`} + updateSection={this.onUpdateInviteIdSection} + /> + ); + } + let nameSection; if (this.props.activeSection === 'name') { - let inputs = []; + const inputs = []; let teamNameLabel = 'Team Name'; if (Utils.isMobile()) { @@ -127,17 +448,17 @@ export default class GeneralTab extends React.Component { submit={this.handleNameSubmit} server_error={serverError} client_error={clientError} - updateSection={this.onUpdateSection} + updateSection={this.onUpdateNameSection} /> ); } else { - let describe = this.state.name; + var describe = this.state.name; nameSection = ( <SettingItemMin title={`Team Name`} describe={describe} - updateSection={this.onUpdateSection} + updateSection={this.onUpdateNameSection} /> ); } @@ -158,16 +479,19 @@ export default class GeneralTab extends React.Component { ref='title' > <i className='modal-back'></i> - General Settings + {'General Settings'} </h4> </div> <div ref='wrapper' className='user-settings' > - <h3 className='tab-header'>General Settings</h3> + <h3 className='tab-header'>{'General Settings'}</h3> <div className='divider-dark first'/> {nameSection} + {openInviteSection} + {teamListingSection} + {inviteSection} <div className='divider-dark'/> </div> </div> @@ -178,6 +502,5 @@ export default class GeneralTab extends React.Component { GeneralTab.propTypes = { updateSection: React.PropTypes.func.isRequired, team: React.PropTypes.object.isRequired, - activeSection: React.PropTypes.string.isRequired, - teamDisplayName: React.PropTypes.string.isRequired + activeSection: React.PropTypes.string.isRequired }; diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx index e14da4f04..09674f1ef 100644 --- a/web/react/components/team_settings.jsx +++ b/web/react/components/team_settings.jsx @@ -37,7 +37,6 @@ export default class TeamSettings extends React.Component { team={this.state.team} activeSection={this.props.activeSection} updateSection={this.props.updateSection} - teamDisplayName={this.props.teamDisplayName} /> </div> ); @@ -72,12 +71,11 @@ export default class TeamSettings extends React.Component { TeamSettings.defaultProps = { activeTab: '', - activeSection: '', - teamDisplayName: '' + activeSection: '' }; + TeamSettings.propTypes = { activeTab: React.PropTypes.string.isRequired, activeSection: React.PropTypes.string.isRequired, - updateSection: React.PropTypes.func.isRequired, - teamDisplayName: React.PropTypes.string.isRequired + updateSection: React.PropTypes.func.isRequired }; diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx index 5c5995020..17fe31c65 100644 --- a/web/react/components/team_settings_modal.jsx +++ b/web/react/components/team_settings_modal.jsx @@ -82,7 +82,6 @@ export default class TeamSettingsModal extends React.Component { activeTab={this.state.activeTab} activeSection={this.state.activeSection} updateSection={this.updateSection} - teamDisplayName={this.props.teamDisplayName} /> </div> </div> @@ -95,5 +94,4 @@ export default class TeamSettingsModal extends React.Component { } TeamSettingsModal.propTypes = { - teamDisplayName: React.PropTypes.string.isRequired }; diff --git a/web/react/pages/channel.jsx b/web/react/pages/channel.jsx index 03e049db0..7a04c5979 100644 --- a/web/react/pages/channel.jsx +++ b/web/react/pages/channel.jsx @@ -90,7 +90,7 @@ function setupChannelPage(props) { ); ReactDOM.render( - <TeamSettingsModal teamDisplayName={props.TeamDisplayName} />, + <TeamSettingsModal />, document.getElementById('team_settings_modal') ); diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx index 430de980c..9865e6fd2 100644 --- a/web/react/pages/login.jsx +++ b/web/react/pages/login.jsx @@ -8,6 +8,7 @@ function setupLoginPage(props) { <Login teamDisplayName={props.TeamDisplayName} teamName={props.TeamName} + inviteId={props.InviteId} />, document.getElementById('login') ); diff --git a/web/react/pages/signup_team.jsx b/web/react/pages/signup_team.jsx index dc8394a77..caa93b5bf 100644 --- a/web/react/pages/signup_team.jsx +++ b/web/react/pages/signup_team.jsx @@ -3,9 +3,19 @@ var SignupTeam = require('../components/signup_team.jsx'); -function setupSignupTeamPage() { +function setupSignupTeamPage(props) { + var teams = []; + + for (var prop in props) { + if (props.hasOwnProperty(prop)) { + if (prop !== 'Title') { + teams.push({name: prop, display_name: props[prop]}); + } + } + } + ReactDOM.render( - <SignupTeam />, + <SignupTeam teams={teams} />, document.getElementById('signup-team') ); } diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx index bf117b3b3..e1dee9c65 100644 --- a/web/react/utils/client.jsx +++ b/web/react/utils/client.jsx @@ -442,16 +442,16 @@ export function inviteMembers(data, success, error) { track('api', 'api_teams_invite_members'); } -export function updateTeamDisplayName(data, success, error) { +export function updateTeam(team, success, error) { $.ajax({ - url: '/api/v1/teams/update_name', + url: '/api/v1/teams/update', dataType: 'json', contentType: 'application/json', type: 'POST', - data: JSON.stringify(data), + data: JSON.stringify(team), success, - error: function onError(xhr, status, err) { - var e = handleError('updateTeamDisplayName', xhr, status, err); + error: (xhr, status, err) => { + var e = handleError('updateTeam', xhr, status, err); error(e); } }); diff --git a/web/sass-files/sass/partials/_signup.scss b/web/sass-files/sass/partials/_signup.scss index 6d0256142..14c676f82 100644 --- a/web/sass-files/sass/partials/_signup.scss +++ b/web/sass-files/sass/partials/_signup.scss @@ -313,6 +313,34 @@ } +.signup-team-all { + width: 280px; + box-shadow: 3px 3px 1px #d5d5d5; + margin: 0px 0px 0px 5px; +} + +.signup-team-dir { + background: #fafafa; + border-bottom: 1px solid #d5d5d5; +} + +.signup-team-dir__group { + padding: 15px 10px 15px 10px; +} + +.signup-team-dir__name { + line-height: 1.3 !important; + font-size: 1.5em !important; + font-weight: 300 !important; +} + +.signup-team-dir__arrow { + float: right; + line-height: 1.3 !important; + font-size: 1.5em !important; + font-weight: 300 !important; +} + .authorize-box { margin: 100px auto; width:500px; diff --git a/web/web.go b/web/web.go index bffe4858e..3cfda39e7 100644 --- a/web/web.go +++ b/web/web.go @@ -152,20 +152,6 @@ func CheckBrowserCompatability(c *api.Context, r *http.Request) bool { } -// func getTeamAndUser(c *api.Context) (*model.Team, *model.User) { -// if tr := <-api.Srv.Store.Team().Get(c.Session.TeamId); tr.Err != nil { -// c.Err = tr.Err -// return nil, nil -// } else { -// if ur := <-api.Srv.Store.User().Get(c.Session.UserId); ur.Err != nil { -// c.Err = ur.Err -// return nil, nil -// } else { -// return tr.Data.(*model.Team), ur.Data.(*model.User) -// } -// } -// } - func root(c *api.Context, w http.ResponseWriter, r *http.Request) { if !CheckBrowserCompatability(c, r) { @@ -174,6 +160,19 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { if len(c.Session.UserId) == 0 { page := NewHtmlTemplatePage("signup_team", "Signup") + + if result := <-api.Srv.Store.Team().GetAllTeamListing(); result.Err != nil { + c.Err = result.Err + return + } else { + teams := result.Data.([]*model.Team) + l4g.Info(teams) + + for _, team := range teams { + page.Props[team.Name] = team.DisplayName + } + } + page.Render(c, w) } else { teamChan := api.Srv.Store.Team().Get(c.Session.TeamId) @@ -240,6 +239,11 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) { page := NewHtmlTemplatePage("login", "Login") page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamName"] = team.Name + + if team.AllowOpenInvite { + page.Props["InviteId"] = team.InviteId + } + page.Render(c, w) } @@ -285,7 +289,7 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) if len(id) > 0 { props = make(map[string]string) - if result := <-api.Srv.Store.Team().Get(id); result.Err != nil { + if result := <-api.Srv.Store.Team().GetByInviteId(id); result.Err != nil { c.Err = result.Err return } else { |