summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--api/export.go2
-rw-r--r--api/team.go48
-rw-r--r--api/team_test.go33
-rw-r--r--api/user.go91
-rw-r--r--api/user_test.go21
-rw-r--r--model/client.go12
-rw-r--r--model/team.go20
-rw-r--r--store/sql_session_store.go18
-rw-r--r--store/sql_session_store_test.go23
-rw-r--r--store/sql_team_store.go4
-rw-r--r--store/sql_user_store.go19
-rw-r--r--store/sql_user_store_test.go18
-rw-r--r--store/store.go4
-rw-r--r--web/react/components/admin_console/admin_controller.jsx84
-rw-r--r--web/react/components/admin_console/admin_sidebar.jsx179
-rw-r--r--web/react/components/admin_console/reset_password_modal.jsx132
-rw-r--r--web/react/components/admin_console/select_team_modal.jsx193
-rw-r--r--web/react/components/admin_console/team_users.jsx178
-rw-r--r--web/react/components/admin_console/user_item.jsx266
-rw-r--r--web/react/components/channel_loader.jsx9
-rw-r--r--web/react/components/channel_notifications.jsx16
-rw-r--r--web/react/components/create_comment.jsx7
-rw-r--r--web/react/components/create_post.jsx7
-rw-r--r--web/react/components/file_attachment.jsx10
-rw-r--r--web/react/components/file_attachment_list.jsx16
-rw-r--r--web/react/components/file_upload.jsx3
-rw-r--r--web/react/components/post_body.jsx1
-rw-r--r--web/react/components/post_info.jsx2
-rw-r--r--web/react/components/rhs_comment.jsx6
-rw-r--r--web/react/components/rhs_root_post.jsx6
-rw-r--r--web/react/components/settings_sidebar.jsx3
-rw-r--r--web/react/components/team_import_tab.jsx2
-rw-r--r--web/react/components/team_settings_modal.jsx4
-rw-r--r--web/react/components/user_settings/custom_theme_chooser.jsx7
-rw-r--r--web/react/components/user_settings/manage_incoming_hooks.jsx65
-rw-r--r--web/react/components/user_settings/premade_theme_chooser.jsx5
-rw-r--r--web/react/components/user_settings/user_settings_notifications.jsx6
-rw-r--r--web/react/components/view_image.jsx236
-rw-r--r--web/react/components/view_image_popover_bar.jsx66
-rw-r--r--web/react/stores/admin_store.jsx40
-rw-r--r--web/react/utils/async_client.jsx26
-rw-r--r--web/react/utils/client.jsx29
-rw-r--r--web/react/utils/constants.jsx36
-rw-r--r--web/react/utils/emoticons.jsx7
-rw-r--r--web/react/utils/markdown.jsx16
-rw-r--r--web/react/utils/utils.jsx14
-rw-r--r--web/sass-files/sass/partials/_admin-console.scss8
-rw-r--r--web/sass-files/sass/partials/_base.scss14
-rw-r--r--web/sass-files/sass/partials/_files.scss9
-rw-r--r--web/sass-files/sass/partials/_markdown.scss13
-rw-r--r--web/sass-files/sass/partials/_mentions.scss1
-rw-r--r--web/sass-files/sass/partials/_modal.scss42
-rw-r--r--web/sass-files/sass/partials/_post.scss7
-rw-r--r--web/sass-files/sass/partials/_settings.scss15
-rw-r--r--web/static/images/themes/organization.pngbin0 -> 86044 bytes
55 files changed, 1670 insertions, 429 deletions
diff --git a/api/export.go b/api/export.go
index 73142a0e4..aff34073f 100644
--- a/api/export.go
+++ b/api/export.go
@@ -87,7 +87,7 @@ func ExportTeams(writer ExportWriter, options *ExportOptions) *model.AppError {
// Get the teams
var teams []*model.Team
if len(options.TeamsToExport) == 0 {
- if result := <-Srv.Store.Team().GetForExport(); result.Err != nil {
+ if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
return result.Err
} else {
teams = result.Data.([]*model.Team)
diff --git a/api/team.go b/api/team.go
index c9d2412d3..d59b2b484 100644
--- a/api/team.go
+++ b/api/team.go
@@ -25,6 +25,7 @@ func InitTeam(r *mux.Router) {
sr.Handle("/create_from_signup", ApiAppHandler(createTeamFromSignup)).Methods("POST")
sr.Handle("/create_with_sso/{service:[A-Za-z]+}", ApiAppHandler(createTeamFromSSO)).Methods("POST")
sr.Handle("/signup", ApiAppHandler(signupTeam)).Methods("POST")
+ sr.Handle("/all", ApiUserRequired(getAll)).Methods("GET")
sr.Handle("/find_team_by_name", ApiAppHandler(findTeamByName)).Methods("POST")
sr.Handle("/find_teams", ApiAppHandler(findTeams)).Methods("POST")
sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST")
@@ -302,6 +303,53 @@ func isTreamCreationAllowed(c *Context, email string) bool {
return true
}
+func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
+ if !c.HasSystemAdminPermissions("getLogs") {
+ return
+ }
+
+ if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ teams := result.Data.([]*model.Team)
+ m := make(map[string]*model.Team)
+ for _, v := range teams {
+ m[v.Id] = v
+ }
+
+ w.Write([]byte(model.TeamMapToJson(m)))
+ }
+}
+
+func revokeAllSessions(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.MapFromJson(r.Body)
+ id := props["id"]
+
+ if result := <-Srv.Store.Session().Get(id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ session := result.Data.(*model.Session)
+
+ c.LogAudit("revoked_all=" + id)
+
+ if session.IsOAuth {
+ RevokeAccessToken(session.Token)
+ } else {
+ sessionCache.Remove(session.Token)
+
+ if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ w.Write([]byte(model.MapToJson(props)))
+ return
+ }
+ }
+ }
+}
+
func findTeamByName(c *Context, w http.ResponseWriter, r *http.Request) {
m := model.MapFromJson(r.Body)
diff --git a/api/team_test.go b/api/team_test.go
index cd39dacfe..e2a7cf430 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -132,6 +132,39 @@ func TestFindTeamByEmail(t *testing.T) {
}
}
+func TestGetAllTeams(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")
+
+ if _, err := Client.GetAllTeams(); err == nil {
+ t.Fatal("you shouldn't have permissions")
+ }
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, user, model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if r1, err := Client.GetAllTeams(); err != nil {
+ t.Fatal(err)
+ } else {
+ teams := r1.Data.(map[string]*model.Team)
+ if teams[team.Id].Name != team.Name {
+ t.Fatal()
+ }
+ }
+}
+
/*
XXXXXX investigate and fix failing test
diff --git a/api/user.go b/api/user.go
index 40410cc5b..3cce3cdd3 100644
--- a/api/user.go
+++ b/api/user.go
@@ -51,6 +51,7 @@ func InitUser(r *mux.Router) {
sr.Handle("/me", ApiAppHandler(getMe)).Methods("GET")
sr.Handle("/status", ApiUserRequiredActivity(getStatuses, false)).Methods("GET")
sr.Handle("/profiles", ApiUserRequired(getProfiles)).Methods("GET")
+ sr.Handle("/profiles/{id:[A-Za-z0-9]+}", ApiUserRequired(getProfiles)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}", ApiUserRequired(getUser)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/sessions", ApiUserRequired(getSessions)).Methods("GET")
sr.Handle("/{id:[A-Za-z0-9]+}/audits", ApiUserRequired(getAudits)).Methods("GET")
@@ -166,6 +167,19 @@ func CreateUser(c *Context, team *model.Team, user *model.User) *model.User {
if team.Email == user.Email {
user.Roles = model.ROLE_TEAM_ADMIN
channelRole = model.CHANNEL_ROLE_ADMIN
+
+ // Below is a speical case where the first user in the entire
+ // system is granted the system_admin role instead of admin
+ if result := <-Srv.Store.User().GetTotalUsersCount(); result.Err != nil {
+ c.Err = result.Err
+ return nil
+ } else {
+ count := result.Data.(int64)
+ if count <= 0 {
+ user.Roles = model.ROLE_SYSTEM_ADMIN
+ }
+ }
+
} else {
user.Roles = ""
}
@@ -550,13 +564,26 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ id, ok := params["id"]
+ if ok {
+ // You must be system admin to access another team
+ if id != c.Session.TeamId {
+ if !c.HasSystemAdminPermissions("getProfiles") {
+ return
+ }
+ }
- etag := (<-Srv.Store.User().GetEtagForProfiles(c.Session.TeamId)).Data.(string)
+ } else {
+ id = c.Session.TeamId
+ }
+
+ etag := (<-Srv.Store.User().GetEtagForProfiles(id)).Data.(string)
if HandleEtag(etag, w, r) {
return
}
- if result := <-Srv.Store.User().GetProfiles(c.Session.TeamId); result.Err != nil {
+ if result := <-Srv.Store.User().GetProfiles(id); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -924,8 +951,8 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if model.IsInRole(new_roles, model.ROLE_SYSTEM_ADMIN) {
- c.Err = model.NewAppError("updateRoles", "The system_admin role can only be set from the command line", "")
+ if model.IsInRole(new_roles, model.ROLE_SYSTEM_ADMIN) && !c.IsSystemAdmin() {
+ c.Err = model.NewAppError("updateRoles", "The system_admin role can only be set by another system admin", "")
c.Err.StatusCode = http.StatusForbidden
return
}
@@ -1155,29 +1182,35 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- hash := props["hash"]
- if len(hash) == 0 {
- c.SetInvalidParam("resetPassword", "hash")
+ name := props["name"]
+ if len(name) == 0 {
+ c.SetInvalidParam("resetPassword", "name")
return
}
- data := model.MapFromJson(strings.NewReader(props["data"]))
+ userId := props["user_id"]
+ hash := props["hash"]
+ timeStr := ""
- userId := data["user_id"]
- if len(userId) != 26 {
- c.SetInvalidParam("resetPassword", "data:user_id")
- return
- }
+ if !c.IsSystemAdmin() {
+ if len(hash) == 0 {
+ c.SetInvalidParam("resetPassword", "hash")
+ return
+ }
- timeStr := data["time"]
- if len(timeStr) == 0 {
- c.SetInvalidParam("resetPassword", "data:time")
- return
+ data := model.MapFromJson(strings.NewReader(props["data"]))
+
+ userId = data["user_id"]
+
+ timeStr = data["time"]
+ if len(timeStr) == 0 {
+ c.SetInvalidParam("resetPassword", "data:time")
+ return
+ }
}
- name := props["name"]
- if len(name) == 0 {
- c.SetInvalidParam("resetPassword", "name")
+ if len(userId) != 26 {
+ c.SetInvalidParam("resetPassword", "user_id")
return
}
@@ -1205,15 +1238,17 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) {
- c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "")
- return
- }
+ if !c.IsSystemAdmin() {
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", props["data"], utils.Cfg.EmailSettings.PasswordResetSalt)) {
+ c.Err = model.NewAppError("resetPassword", "The reset password link does not appear to be valid", "")
+ return
+ }
- t, err := strconv.ParseInt(timeStr, 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour
- c.Err = model.NewAppError("resetPassword", "The reset link has expired", "")
- return
+ t, err := strconv.ParseInt(timeStr, 10, 64)
+ if err != nil || model.GetMillis()-t > 1000*60*60 { // one hour
+ c.Err = model.NewAppError("resetPassword", "The reset link has expired", "")
+ return
+ }
}
if result := <-Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil {
diff --git a/api/user_test.go b/api/user_test.go
index 669da4d20..baa567dec 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -228,6 +228,13 @@ func TestGetUser(t *testing.T) {
ruser2, _ := Client.CreateUser(&user2, "")
store.Must(Srv.Store.User().VerifyEmail(ruser2.Data.(*model.User).Id))
+ team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ rteam2, _ := Client.CreateTeam(&team2)
+
+ user3 := model.User{TeamId: rteam2.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
+ ruser3, _ := Client.CreateUser(&user3, "")
+ store.Must(Srv.Store.User().VerifyEmail(ruser3.Data.(*model.User).Id))
+
Client.LoginByEmail(team.Name, user.Email, user.Password)
rId := ruser.Data.(*model.User).Id
@@ -276,13 +283,27 @@ func TestGetUser(t *testing.T) {
t.Log(cache_result.Data)
t.Fatal("cache should be empty")
}
+ }
+ if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err == nil {
+ t.Fatal("shouldn't have access")
}
Client.AuthToken = ""
if _, err := Client.GetUser(ruser2.Data.(*model.User).Id, ""); err == nil {
t.Fatal("shouldn't have accss")
}
+
+ c := &Context{}
+ c.RequestId = model.NewId()
+ c.IpAddress = "cmd_line"
+ UpdateRoles(c, ruser.Data.(*model.User), model.ROLE_SYSTEM_ADMIN)
+
+ Client.LoginByEmail(team.Name, user.Email, "pwd")
+
+ if _, err := Client.GetProfiles(rteam2.Data.(*model.Team).Id, ""); err != nil {
+ t.Fatal(err)
+ }
}
func TestGetAudits(t *testing.T) {
diff --git a/model/client.go b/model/client.go
index cc75ce370..ca17da6d2 100644
--- a/model/client.go
+++ b/model/client.go
@@ -150,6 +150,16 @@ func (c *Client) CreateTeam(team *Team) (*Result, *AppError) {
}
}
+func (c *Client) GetAllTeams() (*Result, *AppError) {
+ if r, err := c.DoApiGet("/teams/all", "", ""); err != nil {
+ return nil, err
+ } else {
+
+ return &Result{r.Header.Get(HEADER_REQUEST_ID),
+ r.Header.Get(HEADER_ETAG_SERVER), TeamMapFromJson(r.Body)}, nil
+ }
+}
+
func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppError) {
m := make(map[string]string)
m["name"] = name
@@ -254,7 +264,7 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) {
}
func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
- if r, err := c.DoApiGet("/users/profiles", "", etag); err != nil {
+ if r, err := c.DoApiGet("/users/profiles/"+teamId, "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
diff --git a/model/team.go b/model/team.go
index 0d740dde2..f80fa3b11 100644
--- a/model/team.go
+++ b/model/team.go
@@ -73,6 +73,26 @@ func TeamFromJson(data io.Reader) *Team {
}
}
+func TeamMapToJson(u map[string]*Team) string {
+ b, err := json.Marshal(u)
+ if err != nil {
+ return ""
+ } else {
+ return string(b)
+ }
+}
+
+func TeamMapFromJson(data io.Reader) map[string]*Team {
+ decoder := json.NewDecoder(data)
+ var teams map[string]*Team
+ err := decoder.Decode(&teams)
+ if err == nil {
+ return teams
+ } else {
+ return nil
+ }
+}
+
func (o *Team) Etag() string {
return Etag(o.Id, o.UpdateAt)
}
diff --git a/store/sql_session_store.go b/store/sql_session_store.go
index c1d2c852b..22411389d 100644
--- a/store/sql_session_store.go
+++ b/store/sql_session_store.go
@@ -140,6 +140,24 @@ func (me SqlSessionStore) Remove(sessionIdOrToken string) StoreChannel {
return storeChannel
}
+func (me SqlSessionStore) RemoveAllSessionsForTeam(teamId string) StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ _, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE TeamId = :TeamId", map[string]interface{}{"TeamId": teamId})
+ if err != nil {
+ result.Err = model.NewAppError("SqlSessionStore.RemoveAllSessionsForTeam", "We couldn't remove all the sessions for the team", "id="+teamId+", err="+err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (me SqlSessionStore) CleanUpExpiredSessions(userId string) StoreChannel {
storeChannel := make(StoreChannel)
diff --git a/store/sql_session_store_test.go b/store/sql_session_store_test.go
index 4ae680556..3d8aafe25 100644
--- a/store/sql_session_store_test.go
+++ b/store/sql_session_store_test.go
@@ -80,6 +80,29 @@ func TestSessionRemove(t *testing.T) {
}
}
+func TestSessionRemoveAll(t *testing.T) {
+ Setup()
+
+ s1 := model.Session{}
+ s1.UserId = model.NewId()
+ s1.TeamId = model.NewId()
+ Must(store.Session().Save(&s1))
+
+ if rs1 := (<-store.Session().Get(s1.Id)); rs1.Err != nil {
+ t.Fatal(rs1.Err)
+ } else {
+ if rs1.Data.(*model.Session).Id != s1.Id {
+ t.Fatal("should match")
+ }
+ }
+
+ Must(store.Session().RemoveAllSessionsForTeam(s1.TeamId))
+
+ if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil {
+ t.Fatal("should have been removed")
+ }
+}
+
func TestSessionRemoveToken(t *testing.T) {
Setup()
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 3d644e577..109fe5401 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -196,7 +196,7 @@ func (s SqlTeamStore) GetTeamsForEmail(email string) StoreChannel {
return storeChannel
}
-func (s SqlTeamStore) GetForExport() StoreChannel {
+func (s SqlTeamStore) GetAll() StoreChannel {
storeChannel := make(StoreChannel)
go func() {
@@ -204,7 +204,7 @@ func (s SqlTeamStore) GetForExport() StoreChannel {
var data []*model.Team
if _, err := s.GetReplica().Select(&data, "SELECT * FROM Teams"); err != nil {
- result.Err = model.NewAppError("SqlTeamStore.GetForExport", "We could not get all teams", err.Error())
+ result.Err = model.NewAppError("SqlTeamStore.GetAllTeams", "We could not get all teams", err.Error())
}
result.Data = data
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 3fd1c82b5..0a723d965 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -481,3 +481,22 @@ func (us SqlUserStore) GetForExport(teamId string) StoreChannel {
return storeChannel
}
+
+func (us SqlUserStore) GetTotalUsersCount() StoreChannel {
+ storeChannel := make(StoreChannel)
+
+ go func() {
+ result := StoreResult{}
+
+ if count, err := us.GetReplica().SelectInt("SELECT COUNT(Id) FROM Users"); err != nil {
+ result.Err = model.NewAppError("SqlUserStore.GetTotalUsersCount", "We could not count the users", err.Error())
+ } else {
+ result.Data = count
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go
index 466da2845..e2a454023 100644
--- a/store/sql_user_store_test.go
+++ b/store/sql_user_store_test.go
@@ -206,6 +206,24 @@ func TestUserStoreGet(t *testing.T) {
}
}
+func TestUserCountt(t *testing.T) {
+ Setup()
+
+ u1 := model.User{}
+ u1.TeamId = model.NewId()
+ u1.Email = model.NewId()
+ Must(store.User().Save(&u1))
+
+ if result := <-store.User().GetTotalUsersCount(); result.Err != nil {
+ t.Fatal(result.Err)
+ } else {
+ count := result.Data.(int64)
+ if count <= 0 {
+ t.Fatal()
+ }
+ }
+}
+
func TestUserStoreGetProfiles(t *testing.T) {
Setup()
diff --git a/store/store.go b/store/store.go
index c9d40cfa5..23580f452 100644
--- a/store/store.go
+++ b/store/store.go
@@ -47,7 +47,7 @@ type TeamStore interface {
Get(id string) StoreChannel
GetByName(name string) StoreChannel
GetTeamsForEmail(domain string) StoreChannel
- GetForExport() StoreChannel
+ GetAll() StoreChannel
}
type ChannelStore interface {
@@ -103,6 +103,7 @@ type UserStore interface {
GetEtagForProfiles(teamId string) StoreChannel
UpdateFailedPasswordAttempts(userId string, attempts int) StoreChannel
GetForExport(teamId string) StoreChannel
+ GetTotalUsersCount() StoreChannel
}
type SessionStore interface {
@@ -110,6 +111,7 @@ type SessionStore interface {
Get(sessionIdOrToken string) StoreChannel
GetSessions(userId string) StoreChannel
Remove(sessionIdOrToken string) StoreChannel
+ RemoveAllSessionsForTeam(teamId string) StoreChannel
UpdateLastActivityAt(sessionId string, time int64) StoreChannel
UpdateRoles(userId string, roles string) StoreChannel
}
diff --git a/web/react/components/admin_console/admin_controller.jsx b/web/react/components/admin_console/admin_controller.jsx
index 6fddfef07..92f0bbdce 100644
--- a/web/react/components/admin_console/admin_controller.jsx
+++ b/web/react/components/admin_console/admin_controller.jsx
@@ -3,6 +3,7 @@
var AdminSidebar = require('./admin_sidebar.jsx');
var AdminStore = require('../../stores/admin_store.jsx');
+var TeamStore = require('../../stores/team_store.jsx');
var AsyncClient = require('../../utils/async_client.jsx');
var LoadingScreen = require('../loading_screen.jsx');
@@ -16,38 +17,104 @@ var GitLabSettingsTab = require('./gitlab_settings.jsx');
var SqlSettingsTab = require('./sql_settings.jsx');
var TeamSettingsTab = require('./team_settings.jsx');
var ServiceSettingsTab = require('./service_settings.jsx');
+var TeamUsersTab = require('./team_users.jsx');
export default class AdminController extends React.Component {
constructor(props) {
super(props);
this.selectTab = this.selectTab.bind(this);
+ this.removeSelectedTeam = this.removeSelectedTeam.bind(this);
+ this.addSelectedTeam = this.addSelectedTeam.bind(this);
this.onConfigListenerChange = this.onConfigListenerChange.bind(this);
+ this.onAllTeamsListenerChange = this.onAllTeamsListenerChange.bind(this);
+
+ var selectedTeams = AdminStore.getSelectedTeams();
+ if (selectedTeams == null) {
+ selectedTeams = {};
+ selectedTeams[TeamStore.getCurrentId()] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+ }
this.state = {
- config: null,
- selected: 'service_settings'
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams,
+ selected: 'service_settings',
+ selectedTeam: null
};
}
componentDidMount() {
AdminStore.addConfigChangeListener(this.onConfigListenerChange);
AsyncClient.getConfig();
+
+ AdminStore.addAllTeamsChangeListener(this.onAllTeamsListenerChange);
+ AsyncClient.getAllTeams();
}
componentWillUnmount() {
AdminStore.removeConfigChangeListener(this.onConfigListenerChange);
+ AdminStore.removeAllTeamsChangeListener(this.onAllTeamsListenerChange);
}
onConfigListenerChange() {
this.setState({
config: AdminStore.getConfig(),
- selected: this.state.selected
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
});
}
- selectTab(tab) {
- this.setState({selected: tab});
+ onAllTeamsListenerChange() {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+
+ });
+ }
+
+ selectTab(tab, teamId) {
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: tab,
+ selectedTeam: teamId
+ });
+ }
+
+ removeSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ Reflect.deleteProperty(selectedTeams, teamId);
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
+ }
+
+ addSelectedTeam(teamId) {
+ var selectedTeams = AdminStore.getSelectedTeams();
+ selectedTeams[teamId] = 'true';
+ AdminStore.saveSelectedTeams(selectedTeams);
+
+ this.setState({
+ config: AdminStore.getConfig(),
+ teams: AdminStore.getAllTeams(),
+ selectedTeams: AdminStore.getSelectedTeams(),
+ selected: this.state.selected,
+ selectedTeam: this.state.selectedTeam
+ });
}
render() {
@@ -74,6 +141,8 @@ export default class AdminController extends React.Component {
tab = <TeamSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'service_settings') {
tab = <ServiceSettingsTab config={this.state.config} />;
+ } else if (this.state.selected === 'team_users') {
+ tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
}
}
@@ -85,7 +154,12 @@ export default class AdminController extends React.Component {
/>
<AdminSidebar
selected={this.state.selected}
+ selectedTeam={this.state.selectedTeam}
selectTab={this.selectTab}
+ teams={this.state.teams}
+ selectedTeams={this.state.selectedTeams}
+ removeSelectedTeam={this.removeSelectedTeam}
+ addSelectedTeam={this.addSelectedTeam}
/>
<div className='inner__wrap channel__wrap'>
<div className='row header'>
diff --git a/web/react/components/admin_console/admin_sidebar.jsx b/web/react/components/admin_console/admin_sidebar.jsx
index 17ce39c7c..cebb3ff20 100644
--- a/web/react/components/admin_console/admin_sidebar.jsx
+++ b/web/react/components/admin_console/admin_sidebar.jsx
@@ -2,6 +2,7 @@
// See License.txt for license information.
var AdminSidebarHeader = require('./admin_sidebar_header.jsx');
+var SelectTeamModal = require('./select_team_modal.jsx');
export default class AdminSidebar extends React.Component {
constructor(props) {
@@ -9,28 +10,121 @@ export default class AdminSidebar extends React.Component {
this.isSelected = this.isSelected.bind(this);
this.handleClick = this.handleClick.bind(this);
+ this.removeTeam = this.removeTeam.bind(this);
+
+ this.showTeamSelect = this.showTeamSelect.bind(this);
+ this.teamSelectedModal = this.teamSelectedModal.bind(this);
+ this.teamSelectedModalDismissed = this.teamSelectedModalDismissed.bind(this);
this.state = {
+ showSelectModal: false
};
}
- handleClick(name, e) {
+ handleClick(name, teamId, e) {
e.preventDefault();
- this.props.selectTab(name);
+ this.props.selectTab(name, teamId);
}
- isSelected(name) {
+ isSelected(name, teamId) {
if (this.props.selected === name) {
- return 'active';
+ if (name === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ return 'active';
+ }
+ } else {
+ return 'active';
+ }
}
return '';
}
+ removeTeam(teamId, e) {
+ e.preventDefault();
+ Reflect.deleteProperty(this.props.selectedTeams, teamId);
+ this.props.removeSelectedTeam(teamId);
+
+ if (this.props.selected === 'team_users') {
+ if (this.props.selectedTeam != null && this.props.selectedTeam === teamId) {
+ this.props.selectTab('service_settings', null);
+ }
+ }
+ }
+
componentDidMount() {
}
+ showTeamSelect(e) {
+ e.preventDefault();
+ this.setState({showSelectModal: true});
+ }
+
+ teamSelectedModal(teamId) {
+ this.props.selectedTeams[teamId] = 'true';
+ this.setState({showSelectModal: false});
+ this.props.addSelectedTeam(teamId);
+ this.forceUpdate();
+ }
+
+ teamSelectedModalDismissed() {
+ this.setState({showSelectModal: false});
+ }
+
render() {
+ var count = '*';
+ var teams = 'Loading';
+
+ if (this.props.teams != null) {
+ count = '' + Object.keys(this.props.teams).length;
+
+ teams = [];
+ for (var key in this.props.selectedTeams) {
+ if (this.props.selectedTeams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+
+ if (team != null) {
+ teams.push(
+ <ul
+ key={'team_' + team.id}
+ className='nav nav__sub-menu'
+ >
+ <li>
+ <a
+ href='#'
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ className={'nav__sub-menu-item ' + this.isSelected('team_users', team.id)}
+ >
+ {team.name}
+ <span
+ className='menu-icon--right menu__close'
+ onClick={this.removeTeam.bind(this, team.id)}
+ style={{cursor: 'pointer'}}
+ >
+ {'x'}
+ </span>
+ </a>
+ </li>
+ <li>
+ <ul className='nav nav__inner-menu'>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('team_users', team.id)}
+ onClick={this.handleClick.bind(this, 'team_users', team.id)}
+ >
+ {'- Users'}
+ </a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ );
+ }
+ }
+ }
+ }
+
return (
<div className='sidebar--left sidebar--collapsable'>
<div>
@@ -39,10 +133,16 @@ export default class AdminSidebar extends React.Component {
<li>
<ul className='nav nav__sub-menu'>
<li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'SETTINGS'}</span>
+ </h4>
+ </li>
+ <li>
<a
href='#'
className={this.isSelected('service_settings')}
- onClick={this.handleClick.bind(this, 'service_settings')}
+ onClick={this.handleClick.bind(this, 'service_settings', null)}
>
{'Service Settings'}
</a>
@@ -51,7 +151,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('team_settings')}
- onClick={this.handleClick.bind(this, 'team_settings')}
+ onClick={this.handleClick.bind(this, 'team_settings', null)}
>
{'Team Settings'}
</a>
@@ -60,7 +160,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('sql_settings')}
- onClick={this.handleClick.bind(this, 'sql_settings')}
+ onClick={this.handleClick.bind(this, 'sql_settings', null)}
>
{'SQL Settings'}
</a>
@@ -69,7 +169,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('email_settings')}
- onClick={this.handleClick.bind(this, 'email_settings')}
+ onClick={this.handleClick.bind(this, 'email_settings', null)}
>
{'Email Settings'}
</a>
@@ -78,7 +178,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('image_settings')}
- onClick={this.handleClick.bind(this, 'image_settings')}
+ onClick={this.handleClick.bind(this, 'image_settings', null)}
>
{'File Settings'}
</a>
@@ -87,7 +187,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('log_settings')}
- onClick={this.handleClick.bind(this, 'log_settings')}
+ onClick={this.handleClick.bind(this, 'log_settings', null)}
>
{'Log Settings'}
</a>
@@ -95,17 +195,8 @@ export default class AdminSidebar extends React.Component {
<li>
<a
href='#'
- className={this.isSelected('logs')}
- onClick={this.handleClick.bind(this, 'logs')}
- >
- {'Logs'}
- </a>
- </li>
- <li>
- <a
- href='#'
className={this.isSelected('rate_settings')}
- onClick={this.handleClick.bind(this, 'rate_settings')}
+ onClick={this.handleClick.bind(this, 'rate_settings', null)}
>
{'Rate Limit Settings'}
</a>
@@ -114,7 +205,7 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('privacy_settings')}
- onClick={this.handleClick.bind(this, 'privacy_settings')}
+ onClick={this.handleClick.bind(this, 'privacy_settings', null)}
>
{'Privacy Settings'}
</a>
@@ -123,21 +214,65 @@ export default class AdminSidebar extends React.Component {
<a
href='#'
className={this.isSelected('gitlab_settings')}
- onClick={this.handleClick.bind(this, 'gitlab_settings')}
+ onClick={this.handleClick.bind(this, 'gitlab_settings', null)}
>
{'GitLab Settings'}
</a>
</li>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'TEAMS (' + count + ')'}</span>
+ <span className='menu-icon--right'>
+ <a
+ href='#'
+ onClick={this.showTeamSelect}
+ >
+ <i className='fa fa-plus'></i>
+ </a>
+ </span>
+ </h4>
+ </li>
+ <li>
+ {teams}
+ </li>
+ <li>
+ <h4>
+ <span className='icon fa fa-gear'></span>
+ <span>{'OTHER'}</span>
+ </h4>
+ </li>
+ <li>
+ <a
+ href='#'
+ className={this.isSelected('logs')}
+ onClick={this.handleClick.bind(this, 'logs', null)}
+ >
+ {'Logs'}
+ </a>
+ </li>
</ul>
</li>
</ul>
</div>
+
+ <SelectTeamModal
+ teams={this.props.teams}
+ show={this.state.showSelectModal}
+ onModalSubmit={this.teamSelectedModal}
+ onModalDismissed={this.teamSelectedModalDismissed}
+ />
</div>
);
}
}
AdminSidebar.propTypes = {
+ teams: React.PropTypes.object,
+ selectedTeams: React.PropTypes.object,
+ removeSelectedTeam: React.PropTypes.func,
+ addSelectedTeam: React.PropTypes.func,
selected: React.PropTypes.string,
+ selectedTeam: React.PropTypes.string,
selectTab: React.PropTypes.func
}; \ No newline at end of file
diff --git a/web/react/components/admin_console/reset_password_modal.jsx b/web/react/components/admin_console/reset_password_modal.jsx
new file mode 100644
index 000000000..0b83edb17
--- /dev/null
+++ b/web/react/components/admin_console/reset_password_modal.jsx
@@ -0,0 +1,132 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Modal = ReactBootstrap.Modal;
+
+export default class ResetPasswordModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
+
+ this.state = {
+ serverError: null
+ };
+ }
+
+ doSubmit(e) {
+ e.preventDefault();
+ var password = React.findDOMNode(this.refs.password).value;
+
+ if (!password || password.length < 5) {
+ this.setState({serverError: 'Please enter at least 5 characters.'});
+ return;
+ }
+
+ this.setState({serverError: null});
+
+ var data = {};
+ data.new_password = password;
+ data.name = this.props.team.name;
+ data.user_id = this.props.user.id;
+
+ Client.resetPassword(data,
+ () => {
+ this.props.onModalSubmit(React.findDOMNode(this.refs.password).value);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ doCancel() {
+ this.setState({serverError: null});
+ this.props.onModalDismissed();
+ }
+
+ render() {
+ if (this.props.user == null) {
+ return <div/>;
+ }
+
+ let urlClass = 'input-group input-group--limit';
+ let serverError = null;
+
+ if (this.state.serverError) {
+ urlClass += ' has-error';
+ serverError = <div className='form-group has-error'><p className='input__help error'>{this.state.serverError}</p></div>;
+ }
+
+ return (
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
+ >
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Reset Password'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
+ >
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-10'>
+ <div className={urlClass}>
+ <span
+ data-toggle='tooltip'
+ title='New Password'
+ className='input-group-addon'
+ >
+ {'New Password'}
+ </span>
+ <input
+ type='password'
+ ref='password'
+ className='form-control'
+ maxLength='22'
+ autoFocus={true}
+ tabIndex='1'
+ />
+ </div>
+ {serverError}
+ </div>
+ </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
+ );
+ }
+}
+
+ResetPasswordModal.defaultProps = {
+ show: false
+};
+
+ResetPasswordModal.propTypes = {
+ user: React.PropTypes.object,
+ team: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/select_team_modal.jsx b/web/react/components/admin_console/select_team_modal.jsx
index fa30de7b2..343f65131 100644
--- a/web/react/components/admin_console/select_team_modal.jsx
+++ b/web/react/components/admin_console/select_team_modal.jsx
@@ -1,124 +1,99 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
-export default class SelectTeam extends React.Component {
+var Modal = ReactBootstrap.Modal;
+
+export default class SelectTeamModal extends React.Component {
constructor(props) {
super(props);
- this.state = {
- };
+ this.doSubmit = this.doSubmit.bind(this);
+ this.doCancel = this.doCancel.bind(this);
}
+ doSubmit(e) {
+ e.preventDefault();
+ this.props.onModalSubmit(React.findDOMNode(this.refs.team).value);
+ }
+ doCancel() {
+ this.props.onModalDismissed();
+ }
render() {
+ if (this.props.teams == null) {
+ return <div/>;
+ }
+
+ var options = [];
+
+ for (var key in this.props.teams) {
+ if (this.props.teams.hasOwnProperty(key)) {
+ var team = this.props.teams[key];
+ options.push(
+ <option
+ key={'opt_' + team.id}
+ value={team.id}
+ >
+ {team.name}
+ </option>
+ );
+ }
+ }
+
return (
- <div className='modal fade'
- id='select-team'
- tabIndex='-1'
- role='dialog'
- aria-labelledby='teamsModalLabel'
+ <Modal
+ show={this.props.show}
+ onHide={this.doCancel}
>
- <div className='modal-dialog'
- role='document'
+ <Modal.Header closeButton={true}>
+ <Modal.Title>{'Select Team'}</Modal.Title>
+ </Modal.Header>
+ <form
+ role='form'
+ className='form-horizontal'
>
- <div className='modal-content'>
- <div className='modal-header'>
- <button
- type='button'
- className='close'
- data-dismiss='modal'
- aria-label='Close'
- >
- <span aria-hidden='true'>&times;</span>
- </button>
- <h4
- className='modal-title'
- id='teamsModalLabel'
- >
- {'Select a team'}
- </h4>
- </div>
- <div className='modal-body'>
- <table className='more-channel-table table'>
- <tbody>
- <tr>
- <td>
- <p className='more-channel-name'>{'Descartes'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Grouping'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Adventure'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Crossroads'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Sky scraping'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Outdoors'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Microsoft'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- <tr>
- <td>
- <p className='more-channel-name'>{'Apple'}</p>
- </td>
- <td className='td--action'>
- <button className='btn btn-primary'>{'Join'}</button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <div className='modal-footer'>
- <button
- type='button'
- className='btn btn-default'
- data-dismiss='modal'
- >
- {'Close'}
- </button>
+ <Modal.Body>
+ <div className='form-group'>
+ <div className='col-sm-12'>
+ <select
+ ref='team'
+ size='10'
+ style={{width: '100%'}}
+ >
+ {options}
+ </select>
+ </div>
</div>
- </div>
- </div>
- </div>
+ </Modal.Body>
+ <Modal.Footer>
+ <button
+ type='button'
+ className='btn btn-default'
+ onClick={this.doCancel}
+ >
+ {'Close'}
+ </button>
+ <button
+ onClick={this.doSubmit}
+ type='submit'
+ className='btn btn-primary'
+ tabIndex='2'
+ >
+ {'Select'}
+ </button>
+ </Modal.Footer>
+ </form>
+ </Modal>
);
}
-} \ No newline at end of file
+}
+
+SelectTeamModal.defaultProps = {
+ show: false
+};
+
+SelectTeamModal.propTypes = {
+ teams: React.PropTypes.object,
+ show: React.PropTypes.bool.isRequired,
+ onModalSubmit: React.PropTypes.func,
+ onModalDismissed: React.PropTypes.func
+};
diff --git a/web/react/components/admin_console/team_users.jsx b/web/react/components/admin_console/team_users.jsx
new file mode 100644
index 000000000..0a971ff15
--- /dev/null
+++ b/web/react/components/admin_console/team_users.jsx
@@ -0,0 +1,178 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var LoadingScreen = require('../loading_screen.jsx');
+var UserItem = require('./user_item.jsx');
+var ResetPasswordModal = require('./reset_password_modal.jsx');
+
+export default class UserList extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.getTeamProfiles = this.getTeamProfiles.bind(this);
+ this.getCurrentTeamProfiles = this.getCurrentTeamProfiles.bind(this);
+ this.doPasswordReset = this.doPasswordReset.bind(this);
+ this.doPasswordResetDismiss = this.doPasswordResetDismiss.bind(this);
+ this.doPasswordResetSubmit = this.doPasswordResetSubmit.bind(this);
+
+ this.state = {
+ teamId: props.team.id,
+ users: null,
+ serverError: null,
+ showPasswordModal: false,
+ user: null
+ };
+ }
+
+ componentDidMount() {
+ this.getCurrentTeamProfiles();
+ }
+
+ getCurrentTeamProfiles() {
+ this.getTeamProfiles(this.props.team.id);
+ }
+
+ // this.setState({
+ // teamId: this.state.teamId,
+ // users: this.state.users,
+ // serverError: this.state.serverError,
+ // showPasswordModal: this.state.showPasswordModal,
+ // user: this.state.user
+ // });
+
+ getTeamProfiles(teamId) {
+ Client.getProfilesForTeam(
+ teamId,
+ (users) => {
+ var memberList = [];
+ for (var id in users) {
+ if (users.hasOwnProperty(id)) {
+ memberList.push(users[id]);
+ }
+ }
+
+ memberList.sort((a, b) => {
+ if (a.username < b.username) {
+ return -1;
+ }
+
+ if (a.username > b.username) {
+ return 1;
+ }
+
+ return 0;
+ });
+
+ this.setState({
+ teamId: this.state.teamId,
+ users: memberList,
+ serverError: this.state.serverError,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ },
+ (err) => {
+ this.setState({
+ teamId: this.state.teamId,
+ users: null,
+ serverError: err.message,
+ showPasswordModal: this.state.showPasswordModal,
+ user: this.state.user
+ });
+ }
+ );
+ }
+
+ doPasswordReset(user) {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: true,
+ user
+ });
+ }
+
+ doPasswordResetDismiss() {
+ this.state.showPasswordModal = false;
+ this.state.user = null;
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ doPasswordResetSubmit() {
+ this.setState({
+ teamId: this.state.teamId,
+ users: this.state.users,
+ serverError: this.state.serverError,
+ showPasswordModal: false,
+ user: null
+ });
+ }
+
+ componentWillReceiveProps(newProps) {
+ this.getTeamProfiles(newProps.team.id);
+ }
+
+ componentWillUnmount() {
+ }
+
+ render() {
+ var serverError = '';
+ if (this.state.serverError) {
+ serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
+ }
+
+ if (this.state.users == null) {
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name}</h3>
+ {serverError}
+ <LoadingScreen />
+ </div>
+ );
+ }
+
+ var memberList = this.state.users.map((user) => {
+ return (
+ <UserItem
+ key={'user_' + user.id}
+ user={user}
+ refreshProfiles={this.getCurrentTeamProfiles}
+ doPasswordReset={this.doPasswordReset}
+ />);
+ });
+
+ return (
+ <div className='wrapper--fixed'>
+ <h3>{'Users for ' + this.props.team.name + ' (' + this.state.users.length + ')'}</h3>
+ {serverError}
+ <form
+ className='form-horizontal'
+ role='form'
+ >
+ <div className='member-list-holder'>
+ {memberList}
+ </div>
+ </form>
+ <ResetPasswordModal
+ user={this.state.user}
+ show={this.state.showPasswordModal}
+ team={this.props.team}
+ onModalSubmit={this.doPasswordResetSubmit}
+ onModalDismissed={this.doPasswordResetDismiss}
+ />
+ </div>
+ );
+ }
+}
+
+UserList.propTypes = {
+ team: React.PropTypes.object
+};
diff --git a/web/react/components/admin_console/user_item.jsx b/web/react/components/admin_console/user_item.jsx
new file mode 100644
index 000000000..32812e875
--- /dev/null
+++ b/web/react/components/admin_console/user_item.jsx
@@ -0,0 +1,266 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+var Client = require('../../utils/client.jsx');
+var Utils = require('../../utils/utils.jsx');
+
+export default class UserItem extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleMakeMember = this.handleMakeMember.bind(this);
+ this.handleMakeActive = this.handleMakeActive.bind(this);
+ this.handleMakeNotActive = this.handleMakeNotActive.bind(this);
+ this.handleMakeAdmin = this.handleMakeAdmin.bind(this);
+ this.handleMakeSystemAdmin = this.handleMakeSystemAdmin.bind(this);
+ this.handleResetPassword = this.handleResetPassword.bind(this);
+
+ this.state = {};
+ }
+
+ handleMakeMember(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: ''
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, true,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeNotActive(e) {
+ e.preventDefault();
+ Client.updateActive(this.props.user.id, false,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeSystemAdmin(e) {
+ e.preventDefault();
+ const data = {
+ user_id: this.props.user.id,
+ new_roles: 'system_admin'
+ };
+
+ Client.updateRoles(data,
+ () => {
+ this.props.refreshProfiles();
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleResetPassword(e) {
+ e.preventDefault();
+ this.props.doPasswordReset(this.props.user);
+ }
+
+ render() {
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='has-error'>
+ <label className='has-error control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ const user = this.props.user;
+ let currentRoles = 'Member';
+ if (user.roles.length > 0) {
+ if (user.roles.indexOf('system_admin') > -1) {
+ currentRoles = 'System Admin';
+ } else {
+ currentRoles = user.roles.charAt(0).toUpperCase() + user.roles.slice(1);
+ }
+ }
+
+ const email = user.email;
+ let showMakeMember = user.roles === 'admin' || user.roles === 'system_admin';
+ let showMakeAdmin = user.roles === '' || user.roles === 'system_admin';
+ let showMakeSystemAdmin = user.roles === '' || user.roles === 'admin';
+ let showMakeActive = false;
+ let showMakeNotActive = user.roles !== 'system_admin';
+
+ if (user.delete_at > 0) {
+ currentRoles = 'Inactive';
+ currentRoles = 'Inactive';
+ showMakeMember = false;
+ showMakeAdmin = false;
+ showMakeSystemAdmin = false;
+ showMakeActive = true;
+ showMakeNotActive = false;
+ }
+
+ let makeSystemAdmin = null;
+ if (showMakeSystemAdmin) {
+ makeSystemAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeSystemAdmin}
+ >
+ {'Make System Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeAdmin = null;
+ if (showMakeAdmin) {
+ makeAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeAdmin}
+ >
+ {'Make Admin'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeMember = null;
+ if (showMakeMember) {
+ makeMember = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeMember}
+ >
+ {'Make Member'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeActive = null;
+ if (showMakeActive) {
+ makeActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeActive}
+ >
+ {'Make Active'}
+ </a>
+ </li>
+ );
+ }
+
+ let makeNotActive = null;
+ if (showMakeNotActive) {
+ makeNotActive = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeNotActive}
+ >
+ {'Make Inactive'}
+ </a>
+ </li>
+ );
+ }
+
+ return (
+ <div className='row member-div'>
+ <img
+ className='post-profile-img pull-left'
+ src={`/api/v1/users/${user.id}/image?time=${user.update_at}`}
+ height='36'
+ width='36'
+ />
+ <span className='member-name'>{Utils.getDisplayName(user)}</span>
+ <span className='member-email'>{email}</span>
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ id='channel_header_dropdown'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{currentRoles} </span>
+ <span className='caret'></span>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ aria-labelledby='channel_header_dropdown'
+ >
+ {makeAdmin}
+ {makeMember}
+ {makeActive}
+ {makeNotActive}
+ {makeSystemAdmin}
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleResetPassword}
+ >
+ {'Reset Password'}
+ </a>
+ </li>
+ </ul>
+ </div>
+ {serverError}
+ </div>
+ );
+ }
+}
+
+UserItem.propTypes = {
+ user: React.PropTypes.object.isRequired,
+ refreshProfiles: React.PropTypes.func.isRequired,
+ doPasswordReset: React.PropTypes.func.isRequired
+};
diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx
index 962ba26ee..39c86405c 100644
--- a/web/react/components/channel_loader.jsx
+++ b/web/react/components/channel_loader.jsx
@@ -109,6 +109,13 @@ export default class ChannelLoader extends React.Component {
$('.modal-body').css('overflow-y', 'auto');
$('.modal-body').css('max-height', $(window).height() * 0.7);
});
+
+ /* Prevent backspace from navigating back a page */
+ $(window).on('keydown.preventBackspace', (e) => {
+ if (e.which === 8 && !$(e.target).is('input, textarea')) {
+ e.preventDefault();
+ }
+ });
}
componentWillUnmount() {
clearInterval(this.intervalId);
@@ -123,6 +130,8 @@ export default class ChannelLoader extends React.Component {
$('body').off('mouseenter mouseleave', '.post.post--comment.same--root');
$('.modal').off('show.bs.modal');
+
+ $(window).off('keydown.preventBackspace');
}
onSocketChange(msg) {
if (msg && msg.user_id && msg.user_id !== UserStore.getCurrentId()) {
diff --git a/web/react/components/channel_notifications.jsx b/web/react/components/channel_notifications.jsx
index 83067240d..9eda68b38 100644
--- a/web/react/components/channel_notifications.jsx
+++ b/web/react/components/channel_notifications.jsx
@@ -163,10 +163,22 @@ export default class ChannelNotifications extends React.Component {
}.bind(this);
let curChannel = ChannelStore.get(this.state.channelId);
- let extraInfo = (<span>These settings will override the global notification settings</span>);
+ let extraInfo = (
+ <span>
+ These settings will override the global notification settings.
+ <br/>
+ Desktop notifications are available on Firefox, Safari, and Chrome.
+ </span>
+ );
if (curChannel && curChannel.display_name) {
- extraInfo = (<span>These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel</span>);
+ extraInfo = (
+ <span>
+ These settings will override the global notification settings for the <b>{curChannel.display_name}</b> channel.
+ <br/>
+ Desktop notifications are available on Firefox, Safari, and Chrome.
+ </span>
+ );
}
return (
diff --git a/web/react/components/create_comment.jsx b/web/react/components/create_comment.jsx
index c2fc0dcf3..99f553c0c 100644
--- a/web/react/components/create_comment.jsx
+++ b/web/react/components/create_comment.jsx
@@ -28,6 +28,7 @@ export default class CreateComment extends React.Component {
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
+ this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.getFileCount = this.getFileCount.bind(this);
@@ -178,6 +179,11 @@ export default class CreateComment extends React.Component {
this.setState({serverError: err});
}
}
+ handleTextDrop(text) {
+ const newText = this.state.messageText + text;
+ this.handleUserInput(newText);
+ Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length);
+ }
removePreview(id) {
let previews = this.state.previews;
let uploadsInProgress = this.state.uploadsInProgress;
@@ -264,6 +270,7 @@ export default class CreateComment extends React.Component {
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
+ onTextDrop={this.handleTextDrop}
postType='comment'
channelId={this.props.channelId}
/>
diff --git a/web/react/components/create_post.jsx b/web/react/components/create_post.jsx
index abad60154..595643027 100644
--- a/web/react/components/create_post.jsx
+++ b/web/react/components/create_post.jsx
@@ -31,6 +31,7 @@ export default class CreatePost extends React.Component {
this.handleUploadStart = this.handleUploadStart.bind(this);
this.handleFileUploadComplete = this.handleFileUploadComplete.bind(this);
this.handleUploadError = this.handleUploadError.bind(this);
+ this.handleTextDrop = this.handleTextDrop.bind(this);
this.removePreview = this.removePreview.bind(this);
this.onChange = this.onChange.bind(this);
this.getFileCount = this.getFileCount.bind(this);
@@ -230,6 +231,11 @@ export default class CreatePost extends React.Component {
this.setState({serverError: err});
}
}
+ handleTextDrop(text) {
+ const newText = this.state.messageText + text;
+ this.handleUserInput(newText);
+ Utils.setCaretPosition(React.findDOMNode(this.refs.textbox.refs.message), newText.length);
+ }
removePreview(id) {
let previews = this.state.previews;
let uploadsInProgress = this.state.uploadsInProgress;
@@ -334,6 +340,7 @@ export default class CreatePost extends React.Component {
onUploadStart={this.handleUploadStart}
onFileUpload={this.handleFileUploadComplete}
onUploadError={this.handleUploadError}
+ onTextDrop={this.handleTextDrop}
postType='post'
channelId=''
/>
diff --git a/web/react/components/file_attachment.jsx b/web/react/components/file_attachment.jsx
index c9aa06a97..888f24aa5 100644
--- a/web/react/components/file_attachment.jsx
+++ b/web/react/components/file_attachment.jsx
@@ -143,10 +143,7 @@ export default class FileAttachment extends React.Component {
>
<a className='post-image__thumbnail'
href='#'
- onClick={this.props.handleImageClick}
- data-img-id={this.props.index}
- data-toggle='modal'
- data-target={'#' + this.props.modalId}
+ onClick={() => this.props.handleImageClick(this.props.index)}
>
{thumbnail}
</a>
@@ -187,9 +184,6 @@ FileAttachment.propTypes = {
// the index of this attachment preview in the parent FileAttachmentList
index: React.PropTypes.number.isRequired,
- // the identifier of the modal dialog used to preview files
- modalId: React.PropTypes.string.isRequired,
-
- // handler for when the thumbnail is clicked
+ // handler for when the thumbnail is clicked passed the index above
handleImageClick: React.PropTypes.func
};
diff --git a/web/react/components/file_attachment_list.jsx b/web/react/components/file_attachment_list.jsx
index abe72089a..212d4a958 100644
--- a/web/react/components/file_attachment_list.jsx
+++ b/web/react/components/file_attachment_list.jsx
@@ -11,23 +11,21 @@ export default class FileAttachmentList extends React.Component {
this.handleImageClick = this.handleImageClick.bind(this);
- this.state = {startImgId: 0};
+ this.state = {showPreviewModal: false, startImgId: 0};
}
- handleImageClick(e) {
- this.setState({startImgId: parseInt($(e.target.parentNode).attr('data-img-id'), 10)});
+ handleImageClick(indexClicked) {
+ this.setState({showPreviewModal: true, startImgId: indexClicked});
}
render() {
var filenames = this.props.filenames;
- var modalId = this.props.modalId;
var postFiles = [];
for (var i = 0; i < filenames.length && i < Constants.MAX_DISPLAY_FILES; i++) {
postFiles.push(
<FileAttachment
- key={i}
+ key={'file_attachment_' + i}
filename={filenames[i]}
index={i}
- modalId={modalId}
handleImageClick={this.handleImageClick}
/>
);
@@ -39,9 +37,10 @@ export default class FileAttachmentList extends React.Component {
{postFiles}
</div>
<ViewImageModal
+ show={this.state.showPreviewModal}
+ onModalDismissed={() => this.setState({showPreviewModal: false})}
channelId={this.props.channelId}
userId={this.props.userId}
- modalId={modalId}
startId={this.state.startImgId}
filenames={filenames}
/>
@@ -55,9 +54,6 @@ FileAttachmentList.propTypes = {
// a list of file pathes displayed by this
filenames: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
- // the identifier of the modal dialog used to preview files
- modalId: React.PropTypes.string.isRequired,
-
// the channel that this is part of
channelId: React.PropTypes.string,
diff --git a/web/react/components/file_upload.jsx b/web/react/components/file_upload.jsx
index 3cb284171..3dc4e5de2 100644
--- a/web/react/components/file_upload.jsx
+++ b/web/react/components/file_upload.jsx
@@ -110,7 +110,7 @@ export default class FileUpload extends React.Component {
if (typeof files !== 'string' && files.length) {
this.uploadFiles(files);
} else {
- this.props.onUploadError('Invalid file upload', -1);
+ this.props.onTextDrop(e.originalEvent.dataTransfer.getData('Text'));
}
}
@@ -266,6 +266,7 @@ FileUpload.propTypes = {
getFileCount: React.PropTypes.func,
onFileUpload: React.PropTypes.func,
onUploadStart: React.PropTypes.func,
+ onTextDrop: React.PropTypes.func,
channelId: React.PropTypes.string,
postType: React.PropTypes.string
};
diff --git a/web/react/components/post_body.jsx b/web/react/components/post_body.jsx
index dbbcdc409..6e98e4aba 100644
--- a/web/react/components/post_body.jsx
+++ b/web/react/components/post_body.jsx
@@ -141,7 +141,6 @@ export default class PostBody extends React.Component {
fileAttachmentHolder = (
<FileAttachmentList
filenames={filenames}
- modalId={`view_image_modal_${post.id}`}
channelId={post.channel_id}
userId={post.user_id}
/>
diff --git a/web/react/components/post_info.jsx b/web/react/components/post_info.jsx
index d2a0a4035..c38edf6a2 100644
--- a/web/react/components/post_info.jsx
+++ b/web/react/components/post_info.jsx
@@ -153,7 +153,7 @@ export default class PostInfo extends React.Component {
<li className='post-header-col'>
<time
className='post-profile-time'
- title={new Date(post.create_at).toString()}
+ title={`${utils.displayDate(post.create_at)} at ${utils.displayTime(post.create_at)}`}
>
{utils.displayDateTime(post.create_at)}
</time>
diff --git a/web/react/components/rhs_comment.jsx b/web/react/components/rhs_comment.jsx
index 4d1892a69..5b4694eb1 100644
--- a/web/react/components/rhs_comment.jsx
+++ b/web/react/components/rhs_comment.jsx
@@ -163,7 +163,6 @@ export default class RhsComment extends React.Component {
fileAttachment = (
<FileAttachmentList
filenames={post.filenames}
- modalId={'rhs_comment_view_image_modal_' + post.id}
channelId={post.channel_id}
userId={post.user_id}
/>
@@ -186,10 +185,7 @@ export default class RhsComment extends React.Component {
<strong><UserProfile userId={post.user_id} /></strong>
</li>
<li className='post-header-col'>
- <time
- className='post-profile-time'
- title={new Date(post.create_at).toString()}
- >
+ <time className='post-profile-time'>
{Utils.displayCommentDateTime(post.create_at)}
</time>
</li>
diff --git a/web/react/components/rhs_root_post.jsx b/web/react/components/rhs_root_post.jsx
index e661bdce1..13ab0c982 100644
--- a/web/react/components/rhs_root_post.jsx
+++ b/web/react/components/rhs_root_post.jsx
@@ -111,7 +111,6 @@ export default class RhsRootPost extends React.Component {
fileAttachment = (
<FileAttachmentList
filenames={post.filenames}
- modalId={'rhs_view_image_modal_' + post.id}
channelId={post.channel_id}
userId={post.user_id}
/>
@@ -133,10 +132,7 @@ export default class RhsRootPost extends React.Component {
<ul className='post-header'>
<li className='post-header-col'><strong><UserProfile userId={post.user_id} /></strong></li>
<li className='post-header-col'>
- <time
- className='post-profile-time'
- title={new Date(post.create_at).toString()}
- >
+ <time className='post-profile-time'>
{utils.displayCommentDateTime(post.create_at)}
</time>
</li>
diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx
index e5cbd6e92..4c4675788 100644
--- a/web/react/components/settings_sidebar.jsx
+++ b/web/react/components/settings_sidebar.jsx
@@ -7,7 +7,8 @@ export default class SettingsSidebar extends React.Component {
this.handleClick = this.handleClick.bind(this);
}
- handleClick(tab) {
+ handleClick(tab, e) {
+ e.preventDefault();
this.props.updateTab(tab.name);
$('.settings-modal').addClass('display--content');
}
diff --git a/web/react/components/team_import_tab.jsx b/web/react/components/team_import_tab.jsx
index 8315430e4..79f03510f 100644
--- a/web/react/components/team_import_tab.jsx
+++ b/web/react/components/team_import_tab.jsx
@@ -35,7 +35,7 @@ export default class TeamImportTab extends React.Component {
var uploadHelpText = (
<div>
<p>{'Slack does not allow you to export files, images, private groups or direct messages stored in Slack. Therefore, Slack import to Mattermost only supports importing of text messages in your Slack team\'\s public channels.'}</p>
- <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import.'}</p>
+ <p>{'The Slack import to Mattermost is in "Preview". Slack bot posts do not yet import and Slack @mentions are not currently supported.'}</p>
</div>
);
diff --git a/web/react/components/team_settings_modal.jsx b/web/react/components/team_settings_modal.jsx
index 0513c811f..8ffbdc6d0 100644
--- a/web/react/components/team_settings_modal.jsx
+++ b/web/react/components/team_settings_modal.jsx
@@ -36,7 +36,9 @@ export default class TeamSettingsModal extends React.Component {
let tabs = [];
tabs.push({name: 'general', uiName: 'General', icon: 'glyphicon glyphicon-cog'});
tabs.push({name: 'import', uiName: 'Import', icon: 'glyphicon glyphicon-upload'});
- tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'});
+
+ // To enable export uncomment this line
+ //tabs.push({name: 'export', uiName: 'Export', icon: 'glyphicon glyphicon-download'});
tabs.push({name: 'feature', uiName: 'Advanced', icon: 'glyphicon glyphicon-wrench'});
return (
diff --git a/web/react/components/user_settings/custom_theme_chooser.jsx b/web/react/components/user_settings/custom_theme_chooser.jsx
index 44630a318..c680d75d1 100644
--- a/web/react/components/user_settings/custom_theme_chooser.jsx
+++ b/web/react/components/user_settings/custom_theme_chooser.jsx
@@ -53,9 +53,12 @@ export default class CustomThemeChooser extends React.Component {
const elements = [];
let colors = '';
- Constants.THEME_ELEMENTS.forEach((element) => {
+ Constants.THEME_ELEMENTS.forEach((element, index) => {
elements.push(
- <div className='col-sm-4 form-group'>
+ <div
+ className='col-sm-4 form-group'
+ key={'custom-theme-key' + index}
+ >
<label className='custom-label'>{element.uiName}</label>
<div
className='input-group color-picker'
diff --git a/web/react/components/user_settings/manage_incoming_hooks.jsx b/web/react/components/user_settings/manage_incoming_hooks.jsx
index df089a403..12c041c7f 100644
--- a/web/react/components/user_settings/manage_incoming_hooks.jsx
+++ b/web/react/components/user_settings/manage_incoming_hooks.jsx
@@ -107,23 +107,23 @@ export default class ManageIncomingHooks extends React.Component {
this.state.hooks.forEach((hook) => {
const c = ChannelStore.get(hook.channel_id);
hooks.push(
- <div>
- <div className='divider-light'></div>
- <span>
- <strong>{'URL: '}</strong>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}
- </span>
- <br/>
- <span>
+ <div className='font--small'>
+ <div className='padding-top x2 divider-light'></div>
+ <div className='padding-top x2'>
+ <strong>{'URL: '}</strong><span className='word-break--all'>{Utils.getWindowLocationOrigin() + '/hooks/' + hook.id}</span>
+ </div>
+ <div className='padding-top'>
<strong>{'Channel: '}</strong>{c.name}
- </span>
- <br/>
- <a
- className={'btn btn-sm btn-primary'}
- href='#'
- onClick={this.removeHook.bind(this, hook.id)}
- >
- {'Remove'}
- </a>
+ </div>
+ <div className='padding-top'>
+ <a
+ className={'text-danger'}
+ href='#'
+ onClick={this.removeHook.bind(this, hook.id)}
+ >
+ {'Remove'}
+ </a>
+ </div>
</div>
);
});
@@ -134,41 +134,38 @@ export default class ManageIncomingHooks extends React.Component {
} else if (hooks.length > 0) {
displayHooks = hooks;
} else {
- displayHooks = <label>{'None'}</label>;
+ displayHooks = <label>{' None'}</label>;
}
const existingHooks = (
- <div>
- <label className='control-label'>{'Existing incoming webhooks'}</label>
- <br/>
+ <div className='padding-top x2'>
+ <label className='control-label padding-top x2'>{'Existing incoming webhooks'}</label>
{displayHooks}
</div>
);
return (
- <div
- key='addIncomingHook'
- className='form-group'
- >
+ <div key='addIncomingHook'>
<label className='control-label'>{'Add a new incoming webhook'}</label>
- <br/>
- <div>
+ <div className='padding-top'>
<select
ref='channelName'
+ className='form-control'
value={this.state.channelId}
onChange={this.updateChannelId}
>
{options}
</select>
- <br/>
{serverError}
- <a
- className={'btn btn-sm btn-primary' + disableButton}
- href='#'
- onClick={this.addNewHook}
- >
- {'Add'}
- </a>
+ <div className='padding-top'>
+ <a
+ className={'btn btn-sm btn-primary' + disableButton}
+ href='#'
+ onClick={this.addNewHook}
+ >
+ {'Add'}
+ </a>
+ </div>
</div>
{existingHooks}
</div>
diff --git a/web/react/components/user_settings/premade_theme_chooser.jsx b/web/react/components/user_settings/premade_theme_chooser.jsx
index e6aa2f5b9..f8f916bd0 100644
--- a/web/react/components/user_settings/premade_theme_chooser.jsx
+++ b/web/react/components/user_settings/premade_theme_chooser.jsx
@@ -23,7 +23,10 @@ export default class PremadeThemeChooser extends React.Component {
}
premadeThemes.push(
- <div className='col-sm-3 premade-themes'>
+ <div
+ className='col-sm-3 premade-themes'
+ key={'premade-theme-key' + k}
+ >
<div
className={activeClass}
onClick={() => this.props.updateTheme(premadeTheme)}
diff --git a/web/react/components/user_settings/user_settings_notifications.jsx b/web/react/components/user_settings/user_settings_notifications.jsx
index 8d364cde7..ba14f019f 100644
--- a/web/react/components/user_settings/user_settings_notifications.jsx
+++ b/web/react/components/user_settings/user_settings_notifications.jsx
@@ -265,9 +265,12 @@ export default class NotificationsTab extends React.Component {
e.preventDefault();
}.bind(this);
+ const extraInfo = <span>{'Desktop notifications are available on Firefox, Safari, and Chrome.'}</span>;
+
desktopSection = (
<SettingItemMax
title='Send desktop notifications'
+ extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
@@ -343,9 +346,12 @@ export default class NotificationsTab extends React.Component {
e.preventDefault();
}.bind(this);
+ const extraInfo = <span>{'Desktop notification sounds are available on Firefox, Safari, Chrome, Internet Explorer, and Edge.'}</span>;
+
soundSection = (
<SettingItemMax
title='Desktop notification sounds'
+ extraInfo={extraInfo}
inputs={inputs}
submit={this.handleSubmit}
server_error={serverError}
diff --git a/web/react/components/view_image.jsx b/web/react/components/view_image.jsx
index dafcdd9f9..e645878c1 100644
--- a/web/react/components/view_image.jsx
+++ b/web/react/components/view_image.jsx
@@ -3,6 +3,8 @@
var Client = require('../utils/client.jsx');
var Utils = require('../utils/utils.jsx');
+var ViewImagePopoverBar = require('./view_image_popover_bar.jsx');
+var Modal = ReactBootstrap.Modal;
export default class ViewImageModal extends React.Component {
constructor(props) {
@@ -16,6 +18,10 @@ export default class ViewImageModal extends React.Component {
this.handleKeyPress = this.handleKeyPress.bind(this);
this.getPublicLink = this.getPublicLink.bind(this);
this.getPreviewImagePath = this.getPreviewImagePath.bind(this);
+ this.onModalShown = this.onModalShown.bind(this);
+ this.onModalHidden = this.onModalHidden.bind(this);
+ this.onMouseEnterImage = this.onMouseEnterImage.bind(this);
+ this.onMouseLeaveImage = this.onMouseLeaveImage.bind(this);
var loaded = [];
var progress = [];
@@ -23,9 +29,20 @@ export default class ViewImageModal extends React.Component {
loaded.push(false);
progress.push(0);
}
- this.state = {imgId: this.props.startId, viewed: false, loaded: loaded, progress: progress, images: {}, fileSizes: {}};
+ this.state = {
+ imgId: this.props.startId,
+ imgHeight: '100%',
+ loaded: loaded,
+ progress: progress,
+ images: {},
+ fileSizes: {},
+ showFooter: false
+ };
}
- handleNext() {
+ handleNext(e) {
+ if (e) {
+ e.stopPropagation();
+ }
var id = this.state.imgId + 1;
if (id > this.props.filenames.length - 1) {
id = 0;
@@ -33,7 +50,10 @@ export default class ViewImageModal extends React.Component {
this.setState({imgId: id});
this.loadImage(id);
}
- handlePrev() {
+ handlePrev(e) {
+ if (e) {
+ e.stopPropagation();
+ }
var id = this.state.imgId - 1;
if (id < 0) {
id = this.props.filenames.length - 1;
@@ -50,15 +70,27 @@ export default class ViewImageModal extends React.Component {
this.handlePrev();
}
}
- componentWillReceiveProps(nextProps) {
+ onModalShown(nextProps) {
this.setState({imgId: nextProps.startId});
+ this.loadImage(nextProps.startId);
+ }
+ onModalHidden() {
+ if (this.refs.video) {
+ var video = React.findDOMNode(this.refs.video);
+ video.pause();
+ video.currentTime = 0;
+ }
+ }
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.show === true && this.props.show === false) {
+ this.onModalShown(nextProps);
+ } else if (nextProps.show === false && this.props.show === true) {
+ this.onModalHidden();
+ }
}
loadImage(id) {
var imgHeight = $(window).height() - 100;
- if (this.state.loaded[id] || this.state.images[id]) {
- $('.modal .modal-image .image-wrapper img').css('max-height', imgHeight);
- return;
- }
+ this.setState({imgHeight});
var filename = this.props.filenames[id];
@@ -68,84 +100,27 @@ export default class ViewImageModal extends React.Component {
if (fileType === 'image') {
var img = new Image();
img.load(this.getPreviewImagePath(filename),
- function load() {
- var progress = this.state.progress;
- progress[id] = img.completedPercentage;
- this.setState({progress: progress});
- }.bind(this));
- img.onload = (function onload(imgid) {
- return function onloadReturn() {
- var loaded = this.state.loaded;
- loaded[imgid] = true;
- this.setState({loaded: loaded});
- $(React.findDOMNode(this.refs.image)).css('max-height', imgHeight);
- }.bind(this);
- }.bind(this)(id));
+ () => {
+ const progress = this.state.progress;
+ progress[id] = img.completedPercentage;
+ this.setState({progress});
+ });
+ img.onload = () => {
+ const loaded = this.state.loaded;
+ loaded[id] = true;
+ this.setState({loaded});
+ };
var images = this.state.images;
images[id] = img;
- this.setState({images: images});
+ this.setState({images});
} else {
// there's nothing to load for non-image files
var loaded = this.state.loaded;
loaded[id] = true;
- this.setState({loaded: loaded});
- }
- }
- componentDidUpdate() {
- if (this.state.loaded[this.state.imgId]) {
- if (this.refs.imageWrap) {
- $(React.findDOMNode(this.refs.imageWrap)).removeClass('default');
- }
+ this.setState({loaded});
}
}
componentDidMount() {
- $('#' + this.props.modalId).on('shown.bs.modal', function onModalShow() {
- this.setState({viewed: true});
- this.loadImage(this.state.imgId);
- }.bind(this));
-
- $('#' + this.props.modalId).on('hidden.bs.modal', function onModalHide() {
- if (this.refs.video) {
- var video = React.findDOMNode(this.refs.video);
- video.pause();
- video.currentTime = 0;
- }
- }.bind(this));
-
- $(React.findDOMNode(this.refs.modal)).click(function onModalClick(e) {
- if (e.target === this || e.target === React.findDOMNode(this.refs.imageBody)) {
- $('.image_modal').modal('hide');
- }
- }.bind(this));
-
- $(React.findDOMNode(this.refs.imageWrap)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
-
- if (this.refs.previewArrowLeft) {
- $(React.findDOMNode(this.refs.previewArrowLeft)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
- }
-
- if (this.refs.previewArrowRight) {
- $(React.findDOMNode(this.refs.previewArrowRight)).hover(
- function onModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).addClass('footer--show');
- }.bind(this), function offModalHover() {
- $(React.findDOMNode(this.refs.imageFooter)).removeClass('footer--show');
- }.bind(this)
- );
- }
-
$(window).on('keyup', this.handleKeyPress);
// keep track of whether or not this component is mounted so we can safely set the state asynchronously
@@ -189,6 +164,12 @@ export default class ViewImageModal extends React.Component {
// only images have proper previews, so just use a placeholder icon for non-images
return Utils.getPreviewImagePathForFileType(fileType);
}
+ onMouseEnterImage() {
+ this.setState({showFooter: true});
+ }
+ onMouseLeaveImage() {
+ this.setState({showFooter: false});
+ }
render() {
if (this.props.filenames.length < 1 || this.props.filenames.length - 1 < this.state.imgId) {
return <div/>;
@@ -299,23 +280,6 @@ export default class ViewImageModal extends React.Component {
bgClass = 'black-bg';
}
- var publicLink = '';
- if (global.window.config.EnablePublicLink === 'true') {
- publicLink = (
- <div>
- <a
- href='#'
- className='public-link text'
- data-title='Public Image'
- onClick={this.getPublicLink}
- >
- Get Public Link
- </a>
- <span className='text'> | </span>
- </div>
- );
- }
-
var leftArrow = '';
var rightArrow = '';
if (this.props.filenames.length > 1) {
@@ -342,65 +306,61 @@ export default class ViewImageModal extends React.Component {
);
}
+ let closeButtonClass = 'modal-close';
+ if (this.state.showFooter) {
+ closeButtonClass += ' modal-close--show';
+ }
+
return (
- <div
- className='modal fade image_modal'
- ref='modal'
- id={this.props.modalId}
- tabIndex='-1'
- role='dialog'
- aria-hidden='true'
+ <Modal
+ show={this.props.show}
+ onHide={this.props.onModalDismissed}
+ className='image_modal'
+ dialogClassName='modal-image'
>
- <div className='modal-dialog modal-image'>
- <div className='modal-content image-content'>
+ <Modal.Body
+ modalClassName='image-body'
+ onClick={this.props.onModalDismissed}
+ >
+ <div
+ className={'image-wrapper ' + bgClass}
+ style={{maxHeight: this.state.imgHeight}}
+ onMouseEnter={this.onMouseEnterImage}
+ onMouseLeave={this.onMouseLeaveImage}
+ onClick={(e) => e.stopPropagation()}
+ >
<div
- ref='imageBody'
- className='modal-body image-body'
- >
- <div
- ref='imageWrap'
- className={'image-wrapper default ' + bgClass}
- >
- <div
- className='modal-close'
- data-dismiss='modal'
- />
- {content}
- <div
- ref='imageFooter'
- className='modal-button-bar'
- >
- <span className='pull-left text'>{'File ' + (this.state.imgId + 1) + ' of ' + this.props.filenames.length}</span>
- <div className='image-links'>
- {publicLink}
- <a
- href={fileUrl}
- download={name}
- className='text'
- >
- Download
- </a>
- </div>
- </div>
- </div>
- {leftArrow}
- {rightArrow}
- </div>
+ className={closeButtonClass}
+ onClick={this.props.onModalDismissed}
+ />
+ {content}
+ <ViewImagePopoverBar
+ show={this.state.showFooter}
+ fileId={this.state.imgId}
+ totalFiles={this.props.filenames.length}
+ filename={name}
+ fileURL={fileUrl}
+ onGetPublicLinkPressed={this.getPublicLink}
+ />
</div>
- </div>
- </div>
+ {leftArrow}
+ {rightArrow}
+ </Modal.Body>
+ </Modal>
);
}
}
ViewImageModal.defaultProps = {
+ show: false,
filenames: [],
- modalId: '',
channelId: '',
userId: '',
startId: 0
};
ViewImageModal.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ onModalDismissed: React.PropTypes.func.isRequired,
filenames: React.PropTypes.array,
modalId: React.PropTypes.string,
channelId: React.PropTypes.string,
diff --git a/web/react/components/view_image_popover_bar.jsx b/web/react/components/view_image_popover_bar.jsx
new file mode 100644
index 000000000..68817d751
--- /dev/null
+++ b/web/react/components/view_image_popover_bar.jsx
@@ -0,0 +1,66 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+export default class ViewImagePopoverBar extends React.Component {
+ constructor(props) {
+ super(props);
+ }
+ render() {
+ var publicLink = '';
+ if (global.window.config.EnablePublicLink === 'true') {
+ publicLink = (
+ <div>
+ <a
+ href='#'
+ className='public-link text'
+ data-title='Public Image'
+ onClick={this.getPublicLink}
+ >
+ {'Get Public Link'}
+ </a>
+ <span className='text'>{' | '}</span>
+ </div>
+ );
+ }
+
+ var footerClass = 'modal-button-bar';
+ if (this.props.show) {
+ footerClass += ' footer--show';
+ }
+
+ return (
+ <div
+ ref='imageFooter'
+ className={footerClass}
+ >
+ <span className='pull-left text'>{'File ' + (this.props.fileId + 1) + ' of ' + this.props.totalFiles}</span>
+ <div className='image-links'>
+ {publicLink}
+ <a
+ href={this.props.fileURL}
+ download={this.props.filename}
+ className='text'
+ >
+ {'Download'}
+ </a>
+ </div>
+ </div>
+ );
+ }
+}
+ViewImagePopoverBar.defaultProps = {
+ show: false,
+ imgId: 0,
+ totalFiles: 0,
+ filename: '',
+ fileURL: ''
+};
+
+ViewImagePopoverBar.propTypes = {
+ show: React.PropTypes.bool.isRequired,
+ fileId: React.PropTypes.number.isRequired,
+ totalFiles: React.PropTypes.number.isRequired,
+ filename: React.PropTypes.string.isRequired,
+ fileURL: React.PropTypes.string.isRequired,
+ onGetPublicLinkPressed: React.PropTypes.func.isRequired
+};
diff --git a/web/react/stores/admin_store.jsx b/web/react/stores/admin_store.jsx
index dd5b60a24..7b2aeb631 100644
--- a/web/react/stores/admin_store.jsx
+++ b/web/react/stores/admin_store.jsx
@@ -4,11 +4,14 @@
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
var EventEmitter = require('events').EventEmitter;
+var BrowserStore = require('../stores/browser_store.jsx');
+
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
var LOG_CHANGE_EVENT = 'log_change';
var CONFIG_CHANGE_EVENT = 'config_change';
+var ALL_TEAMS_EVENT = 'all_team_change';
class AdminStoreClass extends EventEmitter {
constructor() {
@@ -16,6 +19,7 @@ class AdminStoreClass extends EventEmitter {
this.logs = null;
this.config = null;
+ this.teams = null;
this.emitLogChange = this.emitLogChange.bind(this);
this.addLogChangeListener = this.addLogChangeListener.bind(this);
@@ -24,6 +28,10 @@ class AdminStoreClass extends EventEmitter {
this.emitConfigChange = this.emitConfigChange.bind(this);
this.addConfigChangeListener = this.addConfigChangeListener.bind(this);
this.removeConfigChangeListener = this.removeConfigChangeListener.bind(this);
+
+ this.emitAllTeamsChange = this.emitAllTeamsChange.bind(this);
+ this.addAllTeamsChangeListener = this.addAllTeamsChangeListener.bind(this);
+ this.removeAllTeamsChangeListener = this.removeAllTeamsChangeListener.bind(this);
}
emitLogChange() {
@@ -50,6 +58,18 @@ class AdminStoreClass extends EventEmitter {
this.removeListener(CONFIG_CHANGE_EVENT, callback);
}
+ emitAllTeamsChange() {
+ this.emit(ALL_TEAMS_EVENT);
+ }
+
+ addAllTeamsChangeListener(callback) {
+ this.on(ALL_TEAMS_EVENT, callback);
+ }
+
+ removeAllTeamsChangeListener(callback) {
+ this.removeListener(ALL_TEAMS_EVENT, callback);
+ }
+
getLogs() {
return this.logs;
}
@@ -65,6 +85,22 @@ class AdminStoreClass extends EventEmitter {
saveConfig(config) {
this.config = config;
}
+
+ getAllTeams() {
+ return this.teams;
+ }
+
+ saveAllTeams(teams) {
+ this.teams = teams;
+ }
+
+ getSelectedTeams() {
+ return BrowserStore.getItem('seleted_teams');
+ }
+
+ saveSelectedTeams(teams) {
+ BrowserStore.setItem('seleted_teams', teams);
+ }
}
var AdminStore = new AdminStoreClass();
@@ -81,6 +117,10 @@ AdminStoreClass.dispatchToken = AppDispatcher.register((payload) => {
AdminStore.saveConfig(action.config);
AdminStore.emitConfigChange();
break;
+ case ActionTypes.RECIEVED_ALL_TEAMS:
+ AdminStore.saveAllTeams(action.teams);
+ AdminStore.emitAllTeamsChange();
+ break;
default:
}
});
diff --git a/web/react/utils/async_client.jsx b/web/react/utils/async_client.jsx
index ed228f6c4..ab2965000 100644
--- a/web/react/utils/async_client.jsx
+++ b/web/react/utils/async_client.jsx
@@ -371,6 +371,32 @@ export function getConfig() {
);
}
+export function getAllTeams() {
+ if (isCallInProgress('getAllTeams')) {
+ return;
+ }
+
+ callTracker.getAllTeams = utils.getTimestamp();
+ client.getAllTeams(
+ (data, textStatus, xhr) => {
+ callTracker.getAllTeams = 0;
+
+ if (xhr.status === 304 || !data) {
+ return;
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIEVED_ALL_TEAMS,
+ teams: data
+ });
+ },
+ (err) => {
+ callTracker.getAllTeams = 0;
+ dispatchError(err, 'getAllTeams');
+ }
+ );
+}
+
export function findTeams(email) {
if (isCallInProgress('findTeams_' + email)) {
return;
diff --git a/web/react/utils/client.jsx b/web/react/utils/client.jsx
index a19f58e61..63924bff2 100644
--- a/web/react/utils/client.jsx
+++ b/web/react/utils/client.jsx
@@ -347,6 +347,20 @@ export function testEmail(config, success, error) {
});
}
+export function getAllTeams(success, error) {
+ $.ajax({
+ url: '/api/v1/teams/all',
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getAllTeams', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function getMeSynchronous(success, error) {
var currentUser = null;
$.ajax({
@@ -890,6 +904,21 @@ export function getProfiles(success, error) {
});
}
+export function getProfilesForTeam(teamId, success, error) {
+ $.ajax({
+ cache: false,
+ url: '/api/v1/users/profiles/' + teamId,
+ dataType: 'json',
+ contentType: 'application/json',
+ type: 'GET',
+ success,
+ error: function onError(xhr, status, err) {
+ var e = handleError('getProfilesForTeam', xhr, status, err);
+ error(e);
+ }
+ });
+}
+
export function uploadFile(formData, success, error) {
var request = $.ajax({
url: '/api/v1/files/upload',
diff --git a/web/react/utils/constants.jsx b/web/react/utils/constants.jsx
index 41e9e9ca6..90af9beda 100644
--- a/web/react/utils/constants.jsx
+++ b/web/react/utils/constants.jsx
@@ -35,8 +35,8 @@ module.exports = {
RECIEVED_TEAM: null,
RECIEVED_CONFIG: null,
-
RECIEVED_LOGS: null,
+ RECIEVED_ALL_TEAMS: null,
TOGGLE_IMPORT_THEME_MODAL: null
}),
@@ -134,24 +134,24 @@ module.exports = {
buttonBg: '#2389d7',
buttonColor: '#FFFFFF'
},
- slack: {
- type: 'Slack',
- sidebarBg: '#4D394B',
- sidebarText: '#ab9ba9',
- sidebarUnreadText: '#FFFFFF',
- sidebarTextHoverBg: '#3e313c',
- sidebarTextHoverColor: '#ab9ba9',
- sidebarTextActiveBg: '#4c9689',
+ organization: {
+ type: 'Organization',
+ sidebarBg: '#2071a7',
+ sidebarText: '#bfcde8',
+ sidebarUnreadText: '#fff',
+ sidebarTextHoverBg: '#136197',
+ sidebarTextHoverColor: '#bfcde8',
+ sidebarTextActiveBg: '#136197',
sidebarTextActiveColor: '#FFFFFF',
- sidebarHeaderBg: '#4D394B',
+ sidebarHeaderBg: '#2f81b7',
sidebarHeaderTextColor: '#FFFFFF',
- onlineIndicator: '#4c9689',
- mentionBj: '#eb4d5c',
- mentionColor: '#FFFFFF',
- centerChannelBg: '#FFFFFF',
+ onlineIndicator: '#7DBE00',
+ mentionBj: '#136197',
+ mentionColor: '#bfcde8',
+ centerChannelBg: '#f2f4f8',
centerChannelColor: '#333333',
- linkColor: '#2389d7',
- buttonBg: '#26a970',
+ linkColor: '#2f81b7',
+ buttonBg: '#1dacfc',
buttonColor: '#FFFFFF'
},
dark: {
@@ -165,13 +165,13 @@ module.exports = {
sidebarTextActiveColor: '#FFFFFF',
sidebarHeaderBg: '#1B2C3E',
sidebarHeaderTextColor: '#FFFFFF',
- onlineIndicator: '#4c9689',
+ onlineIndicator: '#55C5B2',
mentionBj: '#B74A4A',
mentionColor: '#FFFFFF',
centerChannelBg: '#2F3E4E',
centerChannelColor: '#DDDDDD',
linkColor: '#A4FFEB',
- buttonBg: '#2B9C99',
+ buttonBg: '#1dacfc',
buttonColor: '#FFFFFF'
}
},
diff --git a/web/react/utils/emoticons.jsx b/web/react/utils/emoticons.jsx
index 7210201ff..a7c837199 100644
--- a/web/react/utils/emoticons.jsx
+++ b/web/react/utils/emoticons.jsx
@@ -5,15 +5,14 @@ const emoticonPatterns = {
smile: /:-?\)/g, // :)
open_mouth: /:o/gi, // :o
scream: /:-o/gi, // :-o
- smirk: /[:;]-?]/g, // :]
- grinning: /[:;]-?d/gi, // :D
+ smirk: /:-?]/g, // :]
+ grinning: /:-?d/gi, // :D
stuck_out_tongue_closed_eyes: /x-d/gi, // x-d
- stuck_out_tongue_winking_eye: /[:;]-?p/gi, // ;p
+ stuck_out_tongue_winking_eye: /:-?p/gi, // :p
rage: /:-?[\[@]/g, // :@
frowning: /:-?\(/g, // :(
sob: /:['’]-?\(|:&#x27;\(/g, // :`(
kissing_heart: /:-?\*/g, // :*
- wink: /;-?\)/g, // ;)
pensive: /:-?\//g, // :/
confounded: /:-?s/gi, // :s
flushed: /:-?\|/g, // :|
diff --git a/web/react/utils/markdown.jsx b/web/react/utils/markdown.jsx
index c6ffb1871..7e88f8644 100644
--- a/web/react/utils/markdown.jsx
+++ b/web/react/utils/markdown.jsx
@@ -15,6 +15,14 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
this.formattingOptions = formattingOptions;
}
+ br() {
+ if (this.formattingOptions.singleline) {
+ return ' ';
+ }
+
+ return super.br();
+ }
+
heading(text, level, raw) {
const id = `${this.options.headerPrefix}${raw.toLowerCase().replace(/[^\w]+/g, '-')}`;
return `<h${level} id="${id}" class="markdown__heading">${text}</h${level}>`;
@@ -36,6 +44,14 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
return output;
}
+ paragraph(text) {
+ if (this.formattingOptions.singleline) {
+ return `<p class="markdown__paragraph-inline">${text}</p>`;
+ }
+
+ return super.paragraph(text);
+ }
+
table(header, body) {
return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
}
diff --git a/web/react/utils/utils.jsx b/web/react/utils/utils.jsx
index 82bb82d6b..50438c6cf 100644
--- a/web/react/utils/utils.jsx
+++ b/web/react/utils/utils.jsx
@@ -553,7 +553,6 @@ export function applyTheme(theme) {
changeCss('.sidebar--left .nav li>h4, .sidebar--left .add-channel-btn', 'color:' + changeOpacity(theme.sidebarText, 0.8), 1);
changeCss('.sidebar--left .add-channel-btn:hover, .sidebar--left .add-channel-btn:focus', 'color:' + theme.sidebarText, 1);
changeCss('.sidebar--left, .sidebar--right .sidebar--right__header', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 1);
- changeCss('.sidebar--right .sidebar-right__body', 'border-color:' + changeOpacity(theme.sidebarText, 0.2), 2);
changeCss('.sidebar--left .status path', 'fill:' + changeOpacity(theme.sidebarText, 0.5), 1);
}
@@ -607,11 +606,12 @@ export function applyTheme(theme) {
}
if (theme.centerChannelBg) {
- changeCss('.app__content', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.app__content, .markdown__table, .markdown__table tbody tr', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-list .post-list-holder-by-time', 'background:' + theme.centerChannelBg, 1);
changeCss('#post-create', 'background:' + theme.centerChannelBg, 1);
changeCss('.search-bar__container .search__form .search-bar', 'background:' + theme.centerChannelBg, 1);
changeCss('.date-separator .separator__text, .new-separator .separator__text', 'background:' + theme.centerChannelBg, 1);
+ changeCss('.post-image__column .post-image__details', 'background:' + theme.centerChannelBg, 1);
changeCss('.sidebar--right', 'background:' + theme.centerChannelBg, 1);
}
@@ -619,9 +619,14 @@ export function applyTheme(theme) {
changeCss('.app__content', 'color:' + theme.centerChannelColor, 2);
changeCss('#post-create', 'color:' + theme.centerChannelColor, 2);
changeCss('.channel-header .heading', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.markdown__table tbody tr:nth-child(2n)', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
changeCss('.channel-header__info>div.dropdown .header-dropdown__icon', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
changeCss('.channel-header #member_popover', 'color:' + changeOpacity(theme.centerChannelColor, 0.8), 1);
- changeCss('.custom-textarea', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '!important; color: ' + theme.centerChannelColor, 1);
+ changeCss('.custom-textarea, .custom-textarea:focus, .preview-container .preview-div, .post-image__column .post-image__details, .sidebar--right .sidebar-right__body, .markdown__table th, .markdown__table td', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
+ changeCss('.custom-textarea', 'color:' + theme.centerChannelColor, 1);
+ changeCss('.post-image__column', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 2);
+ changeCss('.post-image__column .post-image__details', 'color:' + theme.centerChannelColor, 2);
+ changeCss('.post-image__column a, .post-image__column a:hover, .post-image__column a:focus', 'color:' + theme.centerChannelColor, 1);
changeCss('.search-bar__container .search__form .search-bar', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2) + '; color: ' + theme.centerChannelColor, 2);
changeCss('.search-bar__container .search__form', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.channel-intro .channel-intro__content', 'background:' + changeOpacity(theme.centerChannelColor, 0.05), 1);
@@ -629,7 +634,8 @@ export function applyTheme(theme) {
changeCss('.date-separator .separator__hr, .new-separator .separator__hr, .post-right__container .post.post--root hr, .search-item-container', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.channel-intro', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.2), 1);
changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
- changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ changeCss('.post.current--user .post-body, .post.post--comment.other--root.current--user .post-comment, .post.post--comment.other--root .post-comment, .post.same--root .post-body', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
+ changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1440px){.post.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('@media(max-width: 1800px){.inner__wrap.move--left .post.post--comment.same--root', 'border-color:' + changeOpacity(theme.centerChannelColor, 0.07), 2);
changeCss('.post:hover, .sidebar--right .sidebar--right__header', 'background:' + changeOpacity(theme.centerChannelColor, 0.07), 1);
diff --git a/web/sass-files/sass/partials/_admin-console.scss b/web/sass-files/sass/partials/_admin-console.scss
index 9823d2611..e0019eb9b 100644
--- a/web/sass-files/sass/partials/_admin-console.scss
+++ b/web/sass-files/sass/partials/_admin-console.scss
@@ -19,6 +19,11 @@
background-color: $primary-color;
}
}
+ > h4 {
+ background: #333;
+ padding: 10px 10px;
+ margin-top: 1px;
+ }
}
.menu-icon--right {
vertical-align: top;
@@ -29,10 +34,10 @@
font-size: 13px;
right: -2px;
position: relative;
+ color: #fff;
}
}
&.nav__sub-menu {
- padding: 5px 0;
background: #111;
-webkit-font-smoothing: auto;
li {
@@ -88,6 +93,7 @@
overflow: auto;
background-color: #f1f1f1;
padding: 0 20px 20px;
+ min-height: 600px;
}
.wrapper--fixed {
max-width: 800px;
diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss
index 592d5e62e..87d9b8200 100644
--- a/web/sass-files/sass/partials/_base.scss
+++ b/web/sass-files/sass/partials/_base.scss
@@ -34,8 +34,18 @@ body {
}
}
-b, strong {
- font-weight: 600;
+.popover {
+ color: #333;
+ a {
+ color: $primary-color;
+ &:hover, &:focus {
+ color: $primary-color;
+ }
+ }
+}
+
+.word-break--all {
+ word-break: break-all;
}
a {
diff --git a/web/sass-files/sass/partials/_files.scss b/web/sass-files/sass/partials/_files.scss
index 405265f92..6dbb82810 100644
--- a/web/sass-files/sass/partials/_files.scss
+++ b/web/sass-files/sass/partials/_files.scss
@@ -157,23 +157,23 @@
border-left: 1px solid #ddd;
font-size: 13px;
padding: 7px;
+ color: #333;
.post-image__name {
margin-bottom: 3px;
display: block;
- color: #333;
}
.post-image__download {
display: inline-block;
padding-right: 7px;
cursor: pointer;
- color: #555;
+ @include opacity(0.7);
}
.post-image__type {
- color: grey;
+ @include opacity(0.6);
}
.post-image__size {
margin-left: 4px;
- color: grey;
+ @include opacity(0.6);
}
}
a {
@@ -214,4 +214,3 @@
}
}
}
-
diff --git a/web/sass-files/sass/partials/_markdown.scss b/web/sass-files/sass/partials/_markdown.scss
index c09e9d7b4..de92e9d20 100644
--- a/web/sass-files/sass/partials/_markdown.scss
+++ b/web/sass-files/sass/partials/_markdown.scss
@@ -1,5 +1,11 @@
.markdown__heading {
- font-weight: bold;
+ font-weight: 700;
+}
+.markdown__paragraph-inline {
+ display: inline;
+ + .markdown__paragraph-inline {
+ margin-left: 4px;
+ }
}
.markdown__table {
background: #fff;
@@ -25,4 +31,7 @@ pre {
code {
color: #c7254e;
}
-} \ No newline at end of file
+}
+code {
+ background: #fff;
+}
diff --git a/web/sass-files/sass/partials/_mentions.scss b/web/sass-files/sass/partials/_mentions.scss
index aa893c535..a86cb8a73 100644
--- a/web/sass-files/sass/partials/_mentions.scss
+++ b/web/sass-files/sass/partials/_mentions.scss
@@ -65,6 +65,7 @@
.mention-highlight {
background-color:#fff2bb;
+ color: #333;
}
.mention-link {
diff --git a/web/sass-files/sass/partials/_modal.scss b/web/sass-files/sass/partials/_modal.scss
index 38e9b4174..e4e8b20b6 100644
--- a/web/sass-files/sass/partials/_modal.scss
+++ b/web/sass-files/sass/partials/_modal.scss
@@ -3,6 +3,7 @@
}
.modal {
width: 100%;
+ color: #333;
&.image_modal {
.modal-backdrop.in {
@include opacity(0.7);
@@ -10,12 +11,26 @@
}
a, a:focus, a:hover {
color: #2389D7;
+ &.text-danger {
+ color: #a94442;
+ }
}
- .btn.btn-primary {
- background: #4285f4;
- &:hover, &:focus, &:active {
- background: $primary-color--hover;
- color: #fff;
+ .custom-textarea {
+ color: inherit;
+ border-color: #ccc;
+ &:focus {
+ border-color: #ccc;
+ box-shadow: none;
+ }
+ }
+ .btn {
+ font-size: 13px;
+ &.btn-primary {
+ background: #4285f4;
+ &:hover, &:focus, &:active {
+ background: $primary-color--hover;
+ color: #fff;
+ }
}
}
.info__label {
@@ -181,16 +196,13 @@
position: relative;
max-width: 90%;
min-height: 100px;
- min-width: 320px;
+ min-width: 240px;
@include border-radius(3px);
display: table;
margin: 0 auto;
&:hover {
@include border-radius(3px 3px 0 0);
}
- &:hover .modal-close {
- @include opacity(1);
- }
&.default {
width: 100%;
height: 80%;
@@ -204,8 +216,15 @@
right: -13px;
top: -13px;
@include opacity(0);
+ -webkit-transition: opacity 0.6s;
+ -moz-transition: opacity 0.6s;
+ -o-transition: opacity 0.6s;
+ transition: opacity 0.6s;
cursor: pointer;
z-index: 9999;
+ &.modal-close--show {
+ @include opacity(1);
+ }
}
> a {
background: #FFF;
@@ -217,7 +236,7 @@
max-height: 100%;
}
}
- .image-content {
+ .modal-content{
box-shadow: none;
background: rgba(0, 0, 0, 0);
width: 100%;
@@ -301,6 +320,7 @@
}
}
+
// Invite New Member
.invite {
margin-right: 40px;
@@ -315,4 +335,4 @@
padding-left: 0;
}
}
-} \ No newline at end of file
+}
diff --git a/web/sass-files/sass/partials/_post.scss b/web/sass-files/sass/partials/_post.scss
index d4f02cf4b..e362e8f7a 100644
--- a/web/sass-files/sass/partials/_post.scss
+++ b/web/sass-files/sass/partials/_post.scss
@@ -2,13 +2,17 @@
white-space:pre-wrap;
word-wrap:break-word;
background:transparent;
- border:1px solid #ccc !important;
+ border:1px solid #ccc;
position:absolute;
top:0px;
height:auto;
resize: none;
line-height:20px;
min-height:36px;
+ &:focus {
+ border-color: #ccc;
+ box-shadow: none;
+ }
}
.bad-connection {
@@ -235,7 +239,6 @@ body.ios {
}
}
textarea {
- border: none;
box-shadow: none;
}
}
diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss
index 8dcd8f35c..3aab05d70 100644
--- a/web/sass-files/sass/partials/_settings.scss
+++ b/web/sass-files/sass/partials/_settings.scss
@@ -32,6 +32,7 @@
}
.settings-table {
display: table;
+ table-layout: fixed;
width: 100%;
> div {
display: table-cell;
@@ -125,6 +126,10 @@
}
}
+ .font--small {
+ font-size: 13px;
+ }
+
.section-describe {
color:grey;
}
@@ -155,8 +160,18 @@
.has-error {
color: #a94442;
}
+ .padding-top {
+ padding-top: 7px;
+ &.x2 {
+ padding-top: 14px;
+ }
+ }
.control-label {
color: #555;
+ font-weight: 600;
+ &.text-left {
+ text-align: left;
+ }
}
hr {
border-color: #ccc;
diff --git a/web/static/images/themes/organization.png b/web/static/images/themes/organization.png
new file mode 100644
index 000000000..1a38bfb34
--- /dev/null
+++ b/web/static/images/themes/organization.png
Binary files differ