Merge branch 'master' into PLT-340
Этот коммит содержится в:
@@ -101,6 +101,12 @@ Joining the Mattermost community is a great way to build relationships with othe
|
||||
- Review the [Mattermost Code Contribution Guidelines](http://docs.mattermost.org/developer/Code-Contribution-Guidelines/index.html) to submit patches for the core product
|
||||
- Consider building tools that help developers and IT professionals manage Mattermost more effectively (API documentation coming in Beta2)
|
||||
|
||||
##### Check out some projects for connecting to Mattermost:
|
||||
|
||||
- [Matterbridge](https://github.com/42wim/matterbridge) - an IRC bridge connecting to Mattermost
|
||||
- [GitLab Integration Service for Mattermost](https://github.com/mattermost/mattermost-integration-gitlab) - connecting GitLab to Mattermost via incoming webhooks
|
||||
- [Giphy Integration Service for Mattermost](https://github.com/mattermost/mattermost-integration-giphy) - connecting Mattermost to Giphy via outgoing webhooks
|
||||
|
||||
#### Have other ideas or suggestions?
|
||||
|
||||
If there’s some other way you’d like to contribute, please contact us at info@mattermost.com. We’d love to meet you!
|
||||
|
||||
62
api/admin.go
62
api/admin.go
@@ -26,7 +26,7 @@ func InitAdmin(r *mux.Router) {
|
||||
sr.Handle("/test_email", ApiUserRequired(testEmail)).Methods("POST")
|
||||
sr.Handle("/client_props", ApiAppHandler(getClientConfig)).Methods("GET")
|
||||
sr.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST")
|
||||
|
||||
sr.Handle("/analytics/{id:[A-Za-z0-9]+}/{name:[A-Za-z0-9_]+}", ApiAppHandler(getAnalytics)).Methods("GET")
|
||||
}
|
||||
|
||||
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -142,3 +142,63 @@ func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
m["SUCCESS"] = "true"
|
||||
w.Write([]byte(model.MapToJson(m)))
|
||||
}
|
||||
|
||||
func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !c.HasSystemAdminPermissions("getAnalytics") {
|
||||
return
|
||||
}
|
||||
|
||||
params := mux.Vars(r)
|
||||
teamId := params["id"]
|
||||
name := params["name"]
|
||||
|
||||
if name == "standard" {
|
||||
var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 3)
|
||||
rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
|
||||
rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
|
||||
rows[2] = &model.AnalyticsRow{"post_count", 0}
|
||||
openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
|
||||
privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
|
||||
postChan := Srv.Store.Post().AnalyticsPostCount(teamId)
|
||||
|
||||
if r := <-openChan; r.Err != nil {
|
||||
c.Err = r.Err
|
||||
return
|
||||
} else {
|
||||
rows[0].Value = float64(r.Data.(int64))
|
||||
}
|
||||
|
||||
if r := <-privateChan; r.Err != nil {
|
||||
c.Err = r.Err
|
||||
return
|
||||
} else {
|
||||
rows[1].Value = float64(r.Data.(int64))
|
||||
}
|
||||
|
||||
if r := <-postChan; r.Err != nil {
|
||||
c.Err = r.Err
|
||||
return
|
||||
} else {
|
||||
rows[2].Value = float64(r.Data.(int64))
|
||||
}
|
||||
|
||||
w.Write([]byte(rows.ToJson()))
|
||||
} else if name == "post_counts_day" {
|
||||
if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {
|
||||
c.Err = r.Err
|
||||
return
|
||||
} else {
|
||||
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
||||
}
|
||||
} else if name == "user_counts_with_posts_day" {
|
||||
if r := <-Srv.Store.Post().AnalyticsUserCountsWithPostsByDay(teamId); r.Err != nil {
|
||||
c.Err = r.Err
|
||||
return
|
||||
} else {
|
||||
w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
|
||||
}
|
||||
} else {
|
||||
c.SetInvalidParam("getAnalytics", "name")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -150,3 +150,151 @@ func TestEmailTest(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAnalyticsStandard(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||
|
||||
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
|
||||
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||
|
||||
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
|
||||
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
|
||||
|
||||
if _, err := Client.GetAnalytics(team.Id, "standard"); err == nil {
|
||||
t.Fatal("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 result, err := Client.GetAnalytics(team.Id, "standard"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rows := result.Data.(model.AnalyticsRows)
|
||||
|
||||
if rows[0].Name != "channel_open_count" {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[0].Value != 2 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[1].Name != "channel_private_count" {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[1].Value != 1 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[2].Name != "post_count" {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
if rows[2].Value != 1 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPostCount(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||
|
||||
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
|
||||
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||
|
||||
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
|
||||
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
|
||||
|
||||
if _, err := Client.GetAnalytics(team.Id, "post_counts_day"); err == nil {
|
||||
t.Fatal("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 result, err := Client.GetAnalytics(team.Id, "post_counts_day"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rows := result.Data.(model.AnalyticsRows)
|
||||
|
||||
if rows[0].Value != 1 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserCountsWithPostsByDay(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
|
||||
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
|
||||
|
||||
user := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user.Id))
|
||||
|
||||
Client.LoginByEmail(team.Name, user.Email, "pwd")
|
||||
|
||||
channel1 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_PRIVATE, TeamId: team.Id}
|
||||
channel1 = Client.Must(Client.CreateChannel(channel1)).Data.(*model.Channel)
|
||||
|
||||
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
|
||||
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
|
||||
|
||||
if _, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err == nil {
|
||||
t.Fatal("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 result, err := Client.GetAnalytics(team.Id, "user_counts_with_posts_day"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rows := result.Data.(model.AnalyticsRows)
|
||||
|
||||
if rows[0].Value != 1 {
|
||||
t.Log(rows.ToJson())
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
22
api/file.go
22
api/file.go
@@ -52,6 +52,8 @@ const (
|
||||
RotatedCCW = 6
|
||||
RotatedCCWMirrored = 7
|
||||
RotatedCW = 8
|
||||
|
||||
MaxImageSize = 4096 * 2160 // 4k resolution
|
||||
)
|
||||
|
||||
var fileInfoCache *utils.Cache = utils.NewLru(1000)
|
||||
@@ -125,6 +127,21 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
uid := model.NewId()
|
||||
|
||||
if model.IsFileExtImage(filepath.Ext(files[i].Filename)) {
|
||||
imageNameList = append(imageNameList, uid+"/"+filename)
|
||||
imageDataList = append(imageDataList, buf.Bytes())
|
||||
|
||||
// Decode image config first to check dimensions before loading the whole thing into memory later on
|
||||
config, _, err := image.DecodeConfig(bytes.NewReader(buf.Bytes()))
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("uploadFile", "Unable to upload image file.", err.Error())
|
||||
return
|
||||
} else if config.Width*config.Height > MaxImageSize {
|
||||
c.Err = model.NewAppError("uploadFile", "Unable to upload image file. File is too large.", err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
path := "teams/" + c.Session.TeamId + "/channels/" + channelId + "/users/" + c.Session.UserId + "/" + uid + "/" + filename
|
||||
|
||||
if err := writeFile(buf.Bytes(), path); err != nil {
|
||||
@@ -132,11 +149,6 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if model.IsFileExtImage(filepath.Ext(files[i].Filename)) {
|
||||
imageNameList = append(imageNameList, uid+"/"+filename)
|
||||
imageDataList = append(imageDataList, buf.Bytes())
|
||||
}
|
||||
|
||||
encName := utils.UrlEncode(filename)
|
||||
|
||||
fileUrl := "/" + channelId + "/" + c.Session.UserId + "/" + uid + "/" + encName
|
||||
|
||||
42
api/post.go
42
api/post.go
@@ -820,45 +820,23 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
plainSearchParams, hashtagSearchParams := model.ParseSearchParams(terms)
|
||||
paramsList := model.ParseSearchParams(terms)
|
||||
channels := []store.StoreChannel{}
|
||||
|
||||
var hchan store.StoreChannel
|
||||
if hashtagSearchParams != nil {
|
||||
hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagSearchParams)
|
||||
for _, params := range paramsList {
|
||||
channels = append(channels, Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, params))
|
||||
}
|
||||
|
||||
var pchan store.StoreChannel
|
||||
if plainSearchParams != nil {
|
||||
pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, plainSearchParams)
|
||||
}
|
||||
|
||||
mainList := &model.PostList{}
|
||||
if hchan != nil {
|
||||
if result := <-hchan; result.Err != nil {
|
||||
posts := &model.PostList{}
|
||||
for _, channel := range channels {
|
||||
if result := <-channel; result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
mainList = result.Data.(*model.PostList)
|
||||
data := result.Data.(*model.PostList)
|
||||
posts.Extend(data)
|
||||
}
|
||||
}
|
||||
|
||||
plainList := &model.PostList{}
|
||||
if pchan != nil {
|
||||
if result := <-pchan; result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
plainList = result.Data.(*model.PostList)
|
||||
}
|
||||
}
|
||||
|
||||
for _, postId := range plainList.Order {
|
||||
if _, ok := mainList.Posts[postId]; !ok {
|
||||
mainList.AddPost(plainList.Posts[postId])
|
||||
mainList.AddOrder(postId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
w.Write([]byte(mainList.ToJson()))
|
||||
w.Write([]byte(posts.ToJson()))
|
||||
}
|
||||
|
||||
@@ -427,12 +427,18 @@ func TestSearchPostsInChannel(t *testing.T) {
|
||||
channel2 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
|
||||
channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel)
|
||||
|
||||
channel3 := &model.Channel{DisplayName: "TestGetPosts", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
|
||||
channel3 = Client.Must(Client.CreateChannel(channel3)).Data.(*model.Channel)
|
||||
|
||||
post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"}
|
||||
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
|
||||
|
||||
post3 := &model.Post{ChannelId: channel2.Id, Message: "other message with no return"}
|
||||
post3 = Client.Must(Client.CreatePost(post3)).Data.(*model.Post)
|
||||
|
||||
post4 := &model.Post{ChannelId: channel3.Id, Message: "other message with no return"}
|
||||
post4 = Client.Must(Client.CreatePost(post4)).Data.(*model.Post)
|
||||
|
||||
if result := Client.Must(Client.SearchPosts("channel:")).Data.(*model.PostList); len(result.Order) != 0 {
|
||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||
}
|
||||
@@ -476,6 +482,10 @@ func TestSearchPostsInChannel(t *testing.T) {
|
||||
if result := Client.Must(Client.SearchPosts("sgtitlereview channel: " + channel2.Name)).Data.(*model.PostList); len(result.Order) != 1 {
|
||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||
}
|
||||
|
||||
if result := Client.Must(Client.SearchPosts("channel: " + channel2.Name + " channel: " + channel3.Name)).Data.(*model.PostList); len(result.Order) != 3 {
|
||||
t.Fatalf("wrong number of posts returned :) %v :) %v", result.Posts, result.Order)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchPostsFromUser(t *testing.T) {
|
||||
@@ -510,11 +520,12 @@ func TestSearchPostsFromUser(t *testing.T) {
|
||||
post2 := &model.Post{ChannelId: channel2.Id, Message: "sgtitlereview\n with return"}
|
||||
post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
|
||||
|
||||
// includes "X has joined the channel" messages for both user2 and user3
|
||||
|
||||
if result := Client.Must(Client.SearchPosts("from: " + user1.Username)).Data.(*model.PostList); len(result.Order) != 1 {
|
||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||
}
|
||||
|
||||
// note that this includes the "User2 has joined the channel" system messages
|
||||
if result := Client.Must(Client.SearchPosts("from: " + user2.Username)).Data.(*model.PostList); len(result.Order) != 3 {
|
||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||
}
|
||||
@@ -526,6 +537,33 @@ func TestSearchPostsFromUser(t *testing.T) {
|
||||
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " in:" + channel1.Name)).Data.(*model.PostList); len(result.Order) != 1 {
|
||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||
}
|
||||
|
||||
user3 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", Nickname: "Corey Hulen", Password: "pwd"}
|
||||
user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User)
|
||||
store.Must(Srv.Store.User().VerifyEmail(user3.Id))
|
||||
|
||||
Client.LoginByEmail(team.Name, user3.Email, "pwd")
|
||||
Client.Must(Client.JoinChannel(channel1.Id))
|
||||
Client.Must(Client.JoinChannel(channel2.Id))
|
||||
|
||||
// wait for the join/leave messages to be created for user3 since they're done asynchronously
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
if result := Client.Must(Client.SearchPosts("from: " + user2.Username)).Data.(*model.PostList); len(result.Order) != 3 {
|
||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||
}
|
||||
|
||||
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " from: " + user3.Username)).Data.(*model.PostList); len(result.Order) != 5 {
|
||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||
}
|
||||
|
||||
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " from: " + user3.Username + " in:" + channel2.Name)).Data.(*model.PostList); len(result.Order) != 3 {
|
||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||
}
|
||||
|
||||
if result := Client.Must(Client.SearchPosts("from: " + user2.Username + " from: " + user3.Username + " in:" + channel2.Name + " joined")).Data.(*model.PostList); len(result.Order) != 2 {
|
||||
t.Fatalf("wrong number of posts returned %v", len(result.Order))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPostsCache(t *testing.T) {
|
||||
|
||||
12
api/user.go
12
api/user.go
@@ -855,6 +855,18 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Decode image config first to check dimensions before loading the whole thing into memory later on
|
||||
config, _, err := image.DecodeConfig(file)
|
||||
if err != nil {
|
||||
c.Err = model.NewAppError("uploadProfileFile", "Could not decode profile image config.", err.Error())
|
||||
return
|
||||
} else if config.Width*config.Height > MaxImageSize {
|
||||
c.Err = model.NewAppError("uploadProfileFile", "Unable to upload profile image. File is too large.", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
file.Seek(0, 0)
|
||||
|
||||
// Decode image into Image object
|
||||
img, _, err := image.Decode(file)
|
||||
if err != nil {
|
||||
|
||||
12
doc/install/Administration.md
Обычный файл
12
doc/install/Administration.md
Обычный файл
@@ -0,0 +1,12 @@
|
||||
# Administration
|
||||
|
||||
This document provides instructions for common administrator tasks
|
||||
|
||||
##### Creating System Administrator account from commandline
|
||||
- If the System Administrator account becomes unavailable, a person leaving the organization for example, you can set a new system admin from the commandline using `./platform -assign_role -team_name="yourteam" -email="you@example.com" -role="system_admin"`.
|
||||
- After assigning the role the user needs to log out and log back in before the System Administrator role is applied.
|
||||
|
||||
##### Deactivating a user
|
||||
|
||||
- Team Admin or System Admin can go to **Main Menu** > **Manage Members** > **Make Inactive** to deactivate a user, which removes them from the team.
|
||||
- To preserve audit history, users are never deleted from the system. It is highly recommended that System Administrators do not attempt to delete users manually from the database, as this may compromise system integrity and ability to upgrade in future.
|
||||
@@ -8,10 +8,22 @@
|
||||
|
||||
#### Common Issues
|
||||
|
||||
##### Error message in logs when attempting to sign-up: `x509: certificate signed by unknown authority`
|
||||
- This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You can resolve this issue by setting up a load balancer like Ngnix. A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority).
|
||||
|
||||
##### Lost System Administrator account
|
||||
- If the System Administrator account becomes unavailable, a person leaving the organization for example, you can set a new system admin from the commandline using `./platform -assign_role -team_name="yourteam" -email="you@example.com" -role="system_admin"`.
|
||||
- After assigning the role the user needs to log out and log back in before the System Administrator role is applied.
|
||||
|
||||
##### Deactivate a user
|
||||
|
||||
- Team Admin or System Admin can go to **Main Menu** > **Manage Members** > **Make Inactive** to deactivate a user, which removes them from the team.
|
||||
- To preserve audit history, users are never deleted from the system. It is highly recommended that System Administrators do not attempt to delete users manually from the database, as this may compromise system integrity and ability to upgrade in future.
|
||||
|
||||
#### Error Messages
|
||||
|
||||
The following is a list of common error messages and solutions:
|
||||
|
||||
###### `Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.`
|
||||
- Message appears in blue bar on team site. Check that [your websocket port is properly configured](https://github.com/mattermost/platform/blob/master/doc/install/Production-Ubuntu.md#set-up-nginx-server).
|
||||
|
||||
|
||||
###### `x509: certificate signed by unknown authority` in server logs when attempting to sign-up
|
||||
- This error may appear when attempt to use a self-signed certificate to setup SSL, which is not yet supported by Mattermost. You can resolve this issue by setting up a load balancer like Ngnix. A ticket exists to [add support for self-signed certificates in future](x509: certificate signed by unknown authority).
|
||||
|
||||
@@ -90,8 +90,9 @@ As mentioned above, Mattermost makes it easy to take integrations written for Sl
|
||||
|
||||
To see samples and community contributions, please visit <http://mattermost.org/webhooks>.
|
||||
|
||||
#### Limitations
|
||||
#### Known Issues
|
||||
|
||||
- The `attachments` payload used in Slack is not yet supported
|
||||
- Overriding of usernames does not yet apply to notifications
|
||||
- Cannot supply `icon_emoji` to override the message icon
|
||||
- Webhook UI fails when connected to deleted channel
|
||||
|
||||
55
model/analytics_row.go
Обычный файл
55
model/analytics_row.go
Обычный файл
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
)
|
||||
|
||||
type AnalyticsRow struct {
|
||||
Name string `json:"name"`
|
||||
Value float64 `json:"value"`
|
||||
}
|
||||
|
||||
type AnalyticsRows []*AnalyticsRow
|
||||
|
||||
func (me *AnalyticsRow) ToJson() string {
|
||||
b, err := json.Marshal(me)
|
||||
if err != nil {
|
||||
return ""
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func AnalyticsRowFromJson(data io.Reader) *AnalyticsRow {
|
||||
decoder := json.NewDecoder(data)
|
||||
var me AnalyticsRow
|
||||
err := decoder.Decode(&me)
|
||||
if err == nil {
|
||||
return &me
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (me AnalyticsRows) ToJson() string {
|
||||
if b, err := json.Marshal(me); err != nil {
|
||||
return "[]"
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func AnalyticsRowsFromJson(data io.Reader) AnalyticsRows {
|
||||
decoder := json.NewDecoder(data)
|
||||
var me AnalyticsRows
|
||||
err := decoder.Decode(&me)
|
||||
if err == nil {
|
||||
return me
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
37
model/analytics_row_test.go
Обычный файл
37
model/analytics_row_test.go
Обычный файл
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAnalyticsRowJson(t *testing.T) {
|
||||
a1 := AnalyticsRow{}
|
||||
a1.Name = "2015-10-12"
|
||||
a1.Value = 12345.0
|
||||
json := a1.ToJson()
|
||||
ra1 := AnalyticsRowFromJson(strings.NewReader(json))
|
||||
|
||||
if a1.Name != ra1.Name {
|
||||
t.Fatal("days didn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnalyticsRowsJson(t *testing.T) {
|
||||
a1 := AnalyticsRow{}
|
||||
a1.Name = "2015-10-12"
|
||||
a1.Value = 12345.0
|
||||
|
||||
var a1s AnalyticsRows = make([]*AnalyticsRow, 1)
|
||||
a1s[0] = &a1
|
||||
|
||||
ljson := a1s.ToJson()
|
||||
results := AnalyticsRowsFromJson(strings.NewReader(ljson))
|
||||
|
||||
if a1s[0].Name != results[0].Name {
|
||||
t.Fatal("Ids do not match")
|
||||
}
|
||||
}
|
||||
@@ -416,6 +416,15 @@ func (c *Client) TestEmail(config *Config) (*Result, *AppError) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetAnalytics(teamId, name string) (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/admin/analytics/"+teamId+"/"+name, "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
r.Header.Get(HEADER_ETAG_SERVER), AnalyticsRowsFromJson(r.Body)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
|
||||
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -23,6 +23,13 @@ type IncomingWebhook struct {
|
||||
TeamId string `json:"team_id"`
|
||||
}
|
||||
|
||||
type IncomingWebhookRequest struct {
|
||||
Text string `json:"text"`
|
||||
Username string `json:"username"`
|
||||
IconURL string `json:"icon_url"`
|
||||
ChannelName string `json:"channel"`
|
||||
}
|
||||
|
||||
func (o *IncomingWebhook) ToJson() string {
|
||||
b, err := json.Marshal(o)
|
||||
if err != nil {
|
||||
@@ -104,3 +111,14 @@ func (o *IncomingWebhook) PreSave() {
|
||||
func (o *IncomingWebhook) PreUpdate() {
|
||||
o.UpdateAt = GetMillis()
|
||||
}
|
||||
|
||||
func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest {
|
||||
decoder := json.NewDecoder(data)
|
||||
var o IncomingWebhookRequest
|
||||
err := decoder.Decode(&o)
|
||||
if err == nil {
|
||||
return &o
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,6 +54,15 @@ func (o *PostList) AddPost(post *Post) {
|
||||
o.Posts[post.Id] = post
|
||||
}
|
||||
|
||||
func (o *PostList) Extend(other *PostList) {
|
||||
for _, postId := range other.Order {
|
||||
if _, ok := o.Posts[postId]; !ok {
|
||||
o.AddPost(other.Posts[postId])
|
||||
o.AddOrder(postId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (o *PostList) Etag() string {
|
||||
|
||||
id := "0"
|
||||
|
||||
@@ -34,3 +34,37 @@ func TestPostListJson(t *testing.T) {
|
||||
t.Fatal("failed to serialize")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostListExtend(t *testing.T) {
|
||||
l1 := PostList{}
|
||||
|
||||
p1 := &Post{Id: NewId(), Message: NewId()}
|
||||
l1.AddPost(p1)
|
||||
l1.AddOrder(p1.Id)
|
||||
|
||||
p2 := &Post{Id: NewId(), Message: NewId()}
|
||||
l1.AddPost(p2)
|
||||
l1.AddOrder(p2.Id)
|
||||
|
||||
l2 := PostList{}
|
||||
|
||||
p3 := &Post{Id: NewId(), Message: NewId()}
|
||||
l2.AddPost(p3)
|
||||
l2.AddOrder(p3.Id)
|
||||
|
||||
l2.Extend(&l1)
|
||||
|
||||
if len(l1.Posts) != 2 || len(l1.Order) != 2 {
|
||||
t.Fatal("extending l2 changed l1")
|
||||
} else if len(l2.Posts) != 3 {
|
||||
t.Fatal("failed to extend posts l2")
|
||||
} else if l2.Order[0] != p3.Id || l2.Order[1] != p1.Id || l2.Order[2] != p2.Id {
|
||||
t.Fatal("failed to extend order of l2")
|
||||
}
|
||||
|
||||
if len(l1.Posts) != 2 || len(l1.Order) != 2 {
|
||||
t.Fatal("extending l2 again changed l1")
|
||||
} else if len(l2.Posts) != 3 || len(l2.Order) != 3 {
|
||||
t.Fatal("extending l2 again changed l2")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
)
|
||||
|
||||
type SearchParams struct {
|
||||
Terms string
|
||||
IsHashtag bool
|
||||
InChannel string
|
||||
FromUser string
|
||||
Terms string
|
||||
IsHashtag bool
|
||||
InChannels []string
|
||||
FromUsers []string
|
||||
}
|
||||
|
||||
var searchFlags = [...]string{"from", "channel", "in"}
|
||||
@@ -31,9 +31,9 @@ func splitWords(text string) []string {
|
||||
return words
|
||||
}
|
||||
|
||||
func parseSearchFlags(input []string) ([]string, map[string]string) {
|
||||
func parseSearchFlags(input []string) ([]string, [][2]string) {
|
||||
words := []string{}
|
||||
flags := make(map[string]string)
|
||||
flags := [][2]string{}
|
||||
|
||||
skipNextWord := false
|
||||
for i, word := range input {
|
||||
@@ -52,10 +52,10 @@ func parseSearchFlags(input []string) ([]string, map[string]string) {
|
||||
// check for case insensitive equality
|
||||
if strings.EqualFold(flag, searchFlag) {
|
||||
if value != "" {
|
||||
flags[searchFlag] = value
|
||||
flags = append(flags, [2]string{searchFlag, value})
|
||||
isFlag = true
|
||||
} else if i < len(input)-1 {
|
||||
flags[searchFlag] = input[i+1]
|
||||
flags = append(flags, [2]string{searchFlag, input[i+1]})
|
||||
skipNextWord = true
|
||||
isFlag = true
|
||||
}
|
||||
@@ -75,56 +75,66 @@ func parseSearchFlags(input []string) ([]string, map[string]string) {
|
||||
return words, flags
|
||||
}
|
||||
|
||||
func ParseSearchParams(text string) (*SearchParams, *SearchParams) {
|
||||
func ParseSearchParams(text string) []*SearchParams {
|
||||
words, flags := parseSearchFlags(splitWords(text))
|
||||
|
||||
hashtagTerms := []string{}
|
||||
plainTerms := []string{}
|
||||
hashtagTermList := []string{}
|
||||
plainTermList := []string{}
|
||||
|
||||
for _, word := range words {
|
||||
if validHashtag.MatchString(word) {
|
||||
hashtagTerms = append(hashtagTerms, word)
|
||||
hashtagTermList = append(hashtagTermList, word)
|
||||
} else {
|
||||
plainTerms = append(plainTerms, word)
|
||||
plainTermList = append(plainTermList, word)
|
||||
}
|
||||
}
|
||||
|
||||
inChannel := flags["channel"]
|
||||
if inChannel == "" {
|
||||
inChannel = flags["in"]
|
||||
hashtagTerms := strings.Join(hashtagTermList, " ")
|
||||
plainTerms := strings.Join(plainTermList, " ")
|
||||
|
||||
inChannels := []string{}
|
||||
fromUsers := []string{}
|
||||
|
||||
for _, flagPair := range flags {
|
||||
flag := flagPair[0]
|
||||
value := flagPair[1]
|
||||
|
||||
if flag == "in" || flag == "channel" {
|
||||
inChannels = append(inChannels, value)
|
||||
} else if flag == "from" {
|
||||
fromUsers = append(fromUsers, value)
|
||||
}
|
||||
}
|
||||
|
||||
fromUser := flags["from"]
|
||||
paramsList := []*SearchParams{}
|
||||
|
||||
var plainParams *SearchParams
|
||||
if len(plainTerms) > 0 {
|
||||
plainParams = &SearchParams{
|
||||
Terms: strings.Join(plainTerms, " "),
|
||||
IsHashtag: false,
|
||||
InChannel: inChannel,
|
||||
FromUser: fromUser,
|
||||
}
|
||||
paramsList = append(paramsList, &SearchParams{
|
||||
Terms: plainTerms,
|
||||
IsHashtag: false,
|
||||
InChannels: inChannels,
|
||||
FromUsers: fromUsers,
|
||||
})
|
||||
}
|
||||
|
||||
var hashtagParams *SearchParams
|
||||
if len(hashtagTerms) > 0 {
|
||||
hashtagParams = &SearchParams{
|
||||
Terms: strings.Join(hashtagTerms, " "),
|
||||
IsHashtag: true,
|
||||
InChannel: inChannel,
|
||||
FromUser: fromUser,
|
||||
}
|
||||
paramsList = append(paramsList, &SearchParams{
|
||||
Terms: hashtagTerms,
|
||||
IsHashtag: true,
|
||||
InChannels: inChannels,
|
||||
FromUsers: fromUsers,
|
||||
})
|
||||
}
|
||||
|
||||
// special case for when no terms are specified but we still have a filter
|
||||
if plainParams == nil && hashtagParams == nil && (inChannel != "" || fromUser != "") {
|
||||
plainParams = &SearchParams{
|
||||
Terms: "",
|
||||
IsHashtag: false,
|
||||
InChannel: inChannel,
|
||||
FromUser: fromUser,
|
||||
}
|
||||
if len(plainTerms) == 0 && len(hashtagTerms) == 0 {
|
||||
paramsList = append(paramsList, &SearchParams{
|
||||
Terms: "",
|
||||
IsHashtag: true,
|
||||
InChannels: inChannels,
|
||||
FromUsers: fromUsers,
|
||||
})
|
||||
}
|
||||
|
||||
return plainParams, hashtagParams
|
||||
return paramsList
|
||||
}
|
||||
|
||||
@@ -28,25 +28,25 @@ func TestParseSearchFlags(t *testing.T) {
|
||||
|
||||
if words, flags := parseSearchFlags(splitWords("apple banana from:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
||||
t.Fatalf("got incorrect words %v", words)
|
||||
} else if len(flags) != 1 || flags["from"] != "chan" {
|
||||
} else if len(flags) != 1 || flags[0][0] != "from" || flags[0][1] != "chan" {
|
||||
t.Fatalf("got incorrect flags %v", flags)
|
||||
}
|
||||
|
||||
if words, flags := parseSearchFlags(splitWords("apple banana from: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
||||
t.Fatalf("got incorrect words %v", words)
|
||||
} else if len(flags) != 1 || flags["from"] != "chan" {
|
||||
} else if len(flags) != 1 || flags[0][0] != "from" || flags[0][1] != "chan" {
|
||||
t.Fatalf("got incorrect flags %v", flags)
|
||||
}
|
||||
|
||||
if words, flags := parseSearchFlags(splitWords("apple banana in: chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
||||
t.Fatalf("got incorrect words %v", words)
|
||||
} else if len(flags) != 1 || flags["in"] != "chan" {
|
||||
} else if len(flags) != 1 || flags[0][0] != "in" || flags[0][1] != "chan" {
|
||||
t.Fatalf("got incorrect flags %v", flags)
|
||||
}
|
||||
|
||||
if words, flags := parseSearchFlags(splitWords("apple banana channel:chan")); len(words) != 2 || words[0] != "apple" || words[1] != "banana" {
|
||||
t.Fatalf("got incorrect words %v", words)
|
||||
} else if len(flags) != 1 || flags["channel"] != "chan" {
|
||||
} else if len(flags) != 1 || flags[0][0] != "channel" || flags[0][1] != "chan" {
|
||||
t.Fatalf("got incorrect flags %v", flags)
|
||||
}
|
||||
|
||||
@@ -64,7 +64,14 @@ func TestParseSearchFlags(t *testing.T) {
|
||||
|
||||
if words, flags := parseSearchFlags(splitWords("channel: first in: second from:")); len(words) != 1 || words[0] != "from:" {
|
||||
t.Fatalf("got incorrect words %v", words)
|
||||
} else if len(flags) != 2 || flags["channel"] != "first" || flags["in"] != "second" {
|
||||
} else if len(flags) != 2 || flags[0][0] != "channel" || flags[0][1] != "first" || flags[1][0] != "in" || flags[1][1] != "second" {
|
||||
t.Fatalf("got incorrect flags %v", flags)
|
||||
}
|
||||
|
||||
if words, flags := parseSearchFlags(splitWords("channel: first channel: second from: third from: fourth")); len(words) != 0 {
|
||||
t.Fatalf("got incorrect words %v", words)
|
||||
} else if len(flags) != 4 || flags[0][0] != "channel" || flags[0][1] != "first" || flags[1][0] != "channel" || flags[1][1] != "second" ||
|
||||
flags[2][0] != "from" || flags[2][1] != "third" || flags[3][0] != "from" || flags[3][1] != "fourth" {
|
||||
t.Fatalf("got incorrect flags %v", flags)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func NewSqlChannelStore(sqlStore *SqlStore) ChannelStore {
|
||||
|
||||
func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
|
||||
|
||||
// BEGIN REMOVE AFTER 1.1.0
|
||||
// REMOVE AFTER 1.2 SHIP see PLT-828
|
||||
if s.CreateColumnIfNotExists("ChannelMembers", "NotifyProps", "varchar(2000)", "varchar(2000)", "{}") {
|
||||
// populate NotifyProps from existing NotifyLevel field
|
||||
|
||||
@@ -83,7 +83,6 @@ func (s SqlChannelStore) UpgradeSchemaIfNeeded() {
|
||||
|
||||
s.RemoveColumnIfExists("ChannelMembers", "NotifyLevel")
|
||||
}
|
||||
// END REMOVE AFTER 1.1.0
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) CreateIndexesIfNotExists() {
|
||||
@@ -829,3 +828,31 @@ func (s SqlChannelStore) GetForExport(teamId string) StoreChannel {
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlChannelStore) AnalyticsTypeCount(teamId string, channelType string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
v, err := s.GetReplica().SelectInt(
|
||||
`SELECT
|
||||
COUNT(Id) AS Value
|
||||
FROM
|
||||
Channels
|
||||
WHERE
|
||||
TeamId = :TeamId
|
||||
AND Type = :ChannelType`,
|
||||
map[string]interface{}{"TeamId": teamId, "ChannelType": channelType})
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlChannelStore.AnalyticsTypeCount", "We couldn't get channel type counts", err.Error())
|
||||
} else {
|
||||
result.Data = v
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
@@ -561,6 +561,24 @@ func TestChannelStoreGetMoreChannels(t *testing.T) {
|
||||
if list.Channels[0].Name != o3.Name {
|
||||
t.Fatal("missing channel")
|
||||
}
|
||||
|
||||
if r1 := <-store.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_OPEN); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
if r1.Data.(int64) != 2 {
|
||||
t.Log(r1.Data)
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
}
|
||||
|
||||
if r1 := <-store.Channel().AnalyticsTypeCount(o1.TeamId, model.CHANNEL_PRIVATE); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
if r1.Data.(int64) != 2 {
|
||||
t.Log(r1.Data)
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelStoreGetChannelCounts(t *testing.T) {
|
||||
|
||||
@@ -6,6 +6,7 @@ package store
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/platform/model"
|
||||
@@ -413,10 +414,15 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
queryParams := map[string]interface{}{
|
||||
"TeamId": teamId,
|
||||
"UserId": userId,
|
||||
}
|
||||
|
||||
termMap := map[string]bool{}
|
||||
terms := params.Terms
|
||||
|
||||
if terms == "" && params.InChannel == "" && params.FromUser == "" {
|
||||
if terms == "" && len(params.InChannels) == 0 && len(params.FromUsers) == 0 {
|
||||
result.Data = []*model.Post{}
|
||||
storeChannel <- result
|
||||
return
|
||||
@@ -468,13 +474,45 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
|
||||
ORDER BY CreateAt DESC
|
||||
LIMIT 100`
|
||||
|
||||
if params.InChannel != "" {
|
||||
if len(params.InChannels) > 1 {
|
||||
inClause := ":InChannel0"
|
||||
queryParams["InChannel0"] = params.InChannels[0]
|
||||
|
||||
for i := 1; i < len(params.InChannels); i++ {
|
||||
paramName := "InChannel" + strconv.FormatInt(int64(i), 10)
|
||||
inClause += ", :" + paramName
|
||||
queryParams[paramName] = params.InChannels[i]
|
||||
}
|
||||
|
||||
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name IN ("+inClause+")", 1)
|
||||
} else if len(params.InChannels) == 1 {
|
||||
queryParams["InChannel"] = params.InChannels[0]
|
||||
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "AND Name = :InChannel", 1)
|
||||
} else {
|
||||
searchQuery = strings.Replace(searchQuery, "CHANNEL_FILTER", "", 1)
|
||||
}
|
||||
|
||||
if params.FromUser != "" {
|
||||
if len(params.FromUsers) > 1 {
|
||||
inClause := ":FromUser0"
|
||||
queryParams["FromUser0"] = params.FromUsers[0]
|
||||
|
||||
for i := 1; i < len(params.FromUsers); i++ {
|
||||
paramName := "FromUser" + strconv.FormatInt(int64(i), 10)
|
||||
inClause += ", :" + paramName
|
||||
queryParams[paramName] = params.FromUsers[i]
|
||||
}
|
||||
|
||||
searchQuery = strings.Replace(searchQuery, "POST_FILTER", `
|
||||
AND UserId IN (
|
||||
SELECT
|
||||
Id
|
||||
FROM
|
||||
Users
|
||||
WHERE
|
||||
TeamId = :TeamId
|
||||
AND Username IN (`+inClause+`))`, 1)
|
||||
} else if len(params.FromUsers) == 1 {
|
||||
queryParams["FromUser"] = params.FromUsers[0]
|
||||
searchQuery = strings.Replace(searchQuery, "POST_FILTER", `
|
||||
AND UserId IN (
|
||||
SELECT
|
||||
@@ -506,13 +544,7 @@ func (s SqlPostStore) Search(teamId string, userId string, params *model.SearchP
|
||||
searchQuery = strings.Replace(searchQuery, "SEARCH_CLAUSE", searchClause, 1)
|
||||
}
|
||||
|
||||
queryParams := map[string]interface{}{
|
||||
"TeamId": teamId,
|
||||
"UserId": userId,
|
||||
"Terms": terms,
|
||||
"InChannel": params.InChannel,
|
||||
"FromUser": params.FromUser,
|
||||
}
|
||||
queryParams["Terms"] = terms
|
||||
|
||||
_, err := s.GetReplica().Select(&posts, searchQuery, queryParams)
|
||||
if err != nil {
|
||||
@@ -571,3 +603,152 @@ func (s SqlPostStore) GetForExport(channelId string) StoreChannel {
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlPostStore) AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
query :=
|
||||
`SELECT
|
||||
t1.Name, COUNT(t1.UserId) AS Value
|
||||
FROM
|
||||
(SELECT DISTINCT
|
||||
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
|
||||
Posts.UserId
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
ORDER BY Name DESC) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
LIMIT 30`
|
||||
|
||||
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
|
||||
query =
|
||||
`SELECT
|
||||
TO_CHAR(t1.Name, 'YYYY-MM-DD') AS Name, COUNT(t1.UserId) AS Value
|
||||
FROM
|
||||
(SELECT DISTINCT
|
||||
DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)) AS Name,
|
||||
Posts.UserId
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
ORDER BY Name DESC) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
LIMIT 30`
|
||||
}
|
||||
|
||||
var rows model.AnalyticsRows
|
||||
_, err := s.GetReplica().Select(
|
||||
&rows,
|
||||
query,
|
||||
map[string]interface{}{"TeamId": teamId, "Time": model.GetMillis() - 1000*60*60*24*31})
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlPostStore.AnalyticsUserCountsWithPostsByDay", "We couldn't get user counts with posts", err.Error())
|
||||
} else {
|
||||
result.Data = rows
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlPostStore) AnalyticsPostCountsByDay(teamId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
query :=
|
||||
`SELECT
|
||||
Name, COUNT(Value) AS Value
|
||||
FROM
|
||||
(SELECT
|
||||
DATE(FROM_UNIXTIME(Posts.CreateAt / 1000)) AS Name,
|
||||
'1' AS Value
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
AND Posts.CreateAt >:Time) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
LIMIT 30`
|
||||
|
||||
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
|
||||
query =
|
||||
`SELECT
|
||||
Name, COUNT(Value) AS Value
|
||||
FROM
|
||||
(SELECT
|
||||
TO_CHAR(DATE(TO_TIMESTAMP(Posts.CreateAt / 1000)), 'YYYY-MM-DD') AS Name,
|
||||
'1' AS Value
|
||||
FROM
|
||||
Posts, Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId
|
||||
AND Posts.CreateAt > :Time) AS t1
|
||||
GROUP BY Name
|
||||
ORDER BY Name DESC
|
||||
LIMIT 30`
|
||||
}
|
||||
|
||||
var rows model.AnalyticsRows
|
||||
_, err := s.GetReplica().Select(
|
||||
&rows,
|
||||
query,
|
||||
map[string]interface{}{"TeamId": teamId, "Time": model.GetMillis() - 1000*60*60*24*31})
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCountsByDay", "We couldn't get post counts by day", err.Error())
|
||||
} else {
|
||||
result.Data = rows
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlPostStore) AnalyticsPostCount(teamId string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
v, err := s.GetReplica().SelectInt(
|
||||
`SELECT
|
||||
COUNT(Posts.Id) AS Value
|
||||
FROM
|
||||
Posts,
|
||||
Channels
|
||||
WHERE
|
||||
Posts.ChannelId = Channels.Id
|
||||
AND Channels.TeamId = :TeamId`,
|
||||
map[string]interface{}{"TeamId": teamId})
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlPostStore.AnalyticsPostCount", "We couldn't get post counts", err.Error())
|
||||
} else {
|
||||
result.Data = v
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
@@ -580,3 +580,135 @@ func TestPostStoreSearch(t *testing.T) {
|
||||
t.Fatal("returned wrong search result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserCountsWithPostsByDay(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
t1 := &model.Team{}
|
||||
t1.DisplayName = "DisplayName"
|
||||
t1.Name = "a" + model.NewId() + "b"
|
||||
t1.Email = model.NewId() + "@nowhere.com"
|
||||
t1.Type = model.TEAM_OPEN
|
||||
t1 = Must(store.Team().Save(t1)).(*model.Team)
|
||||
|
||||
c1 := &model.Channel{}
|
||||
c1.TeamId = t1.Id
|
||||
c1.DisplayName = "Channel2"
|
||||
c1.Name = "a" + model.NewId() + "b"
|
||||
c1.Type = model.CHANNEL_OPEN
|
||||
c1 = Must(store.Channel().Save(c1)).(*model.Channel)
|
||||
|
||||
o1 := &model.Post{}
|
||||
o1.ChannelId = c1.Id
|
||||
o1.UserId = model.NewId()
|
||||
o1.CreateAt = model.GetMillis()
|
||||
o1.Message = "a" + model.NewId() + "b"
|
||||
o1 = Must(store.Post().Save(o1)).(*model.Post)
|
||||
|
||||
o1a := &model.Post{}
|
||||
o1a.ChannelId = c1.Id
|
||||
o1a.UserId = model.NewId()
|
||||
o1a.CreateAt = o1.CreateAt
|
||||
o1a.Message = "a" + model.NewId() + "b"
|
||||
o1a = Must(store.Post().Save(o1a)).(*model.Post)
|
||||
|
||||
o2 := &model.Post{}
|
||||
o2.ChannelId = c1.Id
|
||||
o2.UserId = model.NewId()
|
||||
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
|
||||
o2.Message = "a" + model.NewId() + "b"
|
||||
o2 = Must(store.Post().Save(o2)).(*model.Post)
|
||||
|
||||
o2a := &model.Post{}
|
||||
o2a.ChannelId = c1.Id
|
||||
o2a.UserId = o2.UserId
|
||||
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24)
|
||||
o2a.Message = "a" + model.NewId() + "b"
|
||||
o2a = Must(store.Post().Save(o2a)).(*model.Post)
|
||||
|
||||
if r1 := <-store.Post().AnalyticsUserCountsWithPostsByDay(t1.Id); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
row1 := r1.Data.(model.AnalyticsRows)[0]
|
||||
if row1.Value != 2 {
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
|
||||
row2 := r1.Data.(model.AnalyticsRows)[1]
|
||||
if row2.Value != 1 {
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPostCountsByDay(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
t1 := &model.Team{}
|
||||
t1.DisplayName = "DisplayName"
|
||||
t1.Name = "a" + model.NewId() + "b"
|
||||
t1.Email = model.NewId() + "@nowhere.com"
|
||||
t1.Type = model.TEAM_OPEN
|
||||
t1 = Must(store.Team().Save(t1)).(*model.Team)
|
||||
|
||||
c1 := &model.Channel{}
|
||||
c1.TeamId = t1.Id
|
||||
c1.DisplayName = "Channel2"
|
||||
c1.Name = "a" + model.NewId() + "b"
|
||||
c1.Type = model.CHANNEL_OPEN
|
||||
c1 = Must(store.Channel().Save(c1)).(*model.Channel)
|
||||
|
||||
o1 := &model.Post{}
|
||||
o1.ChannelId = c1.Id
|
||||
o1.UserId = model.NewId()
|
||||
o1.CreateAt = model.GetMillis()
|
||||
o1.Message = "a" + model.NewId() + "b"
|
||||
o1 = Must(store.Post().Save(o1)).(*model.Post)
|
||||
|
||||
o1a := &model.Post{}
|
||||
o1a.ChannelId = c1.Id
|
||||
o1a.UserId = model.NewId()
|
||||
o1a.CreateAt = o1.CreateAt
|
||||
o1a.Message = "a" + model.NewId() + "b"
|
||||
o1a = Must(store.Post().Save(o1a)).(*model.Post)
|
||||
|
||||
o2 := &model.Post{}
|
||||
o2.ChannelId = c1.Id
|
||||
o2.UserId = model.NewId()
|
||||
o2.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
|
||||
o2.Message = "a" + model.NewId() + "b"
|
||||
o2 = Must(store.Post().Save(o2)).(*model.Post)
|
||||
|
||||
o2a := &model.Post{}
|
||||
o2a.ChannelId = c1.Id
|
||||
o2a.UserId = o2.UserId
|
||||
o2a.CreateAt = o1.CreateAt - (1000 * 60 * 60 * 24 * 2)
|
||||
o2a.Message = "a" + model.NewId() + "b"
|
||||
o2a = Must(store.Post().Save(o2a)).(*model.Post)
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
t.Log(t1.Id)
|
||||
|
||||
if r1 := <-store.Post().AnalyticsPostCountsByDay(t1.Id); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
row1 := r1.Data.(model.AnalyticsRows)[0]
|
||||
if row1.Value != 2 {
|
||||
t.Log(row1)
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
|
||||
row2 := r1.Data.(model.AnalyticsRows)[1]
|
||||
if row2.Value != 2 {
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
}
|
||||
|
||||
if r1 := <-store.Post().AnalyticsPostCount(t1.Id); r1.Err != nil {
|
||||
t.Fatal(r1.Err)
|
||||
} else {
|
||||
if r1.Data.(int64) != 4 {
|
||||
t.Fatal("wrong value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,8 @@ func NewSqlStore() Store {
|
||||
}
|
||||
|
||||
schemaVersion := sqlStore.GetCurrentSchemaVersion()
|
||||
isSchemaVersion07 := false
|
||||
isSchemaVersion07 := false // REMOVE AFTER 1.2 SHIP see PLT-828
|
||||
isSchemaVersion10 := false // REMOVE AFTER 1.2 SHIP see PLT-828
|
||||
|
||||
// If the version is already set then we are potentially in an 'upgrade needed' state
|
||||
if schemaVersion != "" {
|
||||
@@ -86,7 +87,11 @@ func NewSqlStore() Store {
|
||||
isSchemaVersion07 = true
|
||||
}
|
||||
|
||||
if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 {
|
||||
if schemaVersion == "1.0.0" {
|
||||
isSchemaVersion10 = true
|
||||
}
|
||||
|
||||
if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 || isSchemaVersion10 {
|
||||
l4g.Warn("The database schema version of " + schemaVersion + " appears to be out of date")
|
||||
l4g.Warn("Attempting to upgrade the database schema version to " + model.CurrentVersion)
|
||||
} else {
|
||||
@@ -98,7 +103,7 @@ func NewSqlStore() Store {
|
||||
}
|
||||
}
|
||||
|
||||
// REMOVE in 1.2
|
||||
// REMOVE AFTER 1.2 SHIP see PLT-828
|
||||
if sqlStore.DoesTableExist("Sessions") {
|
||||
if sqlStore.DoesColumnExist("Sessions", "AltId") {
|
||||
sqlStore.GetMaster().Exec("DROP TABLE IF EXISTS Sessions")
|
||||
@@ -140,7 +145,7 @@ func NewSqlStore() Store {
|
||||
sqlStore.webhook.(*SqlWebhookStore).CreateIndexesIfNotExists()
|
||||
sqlStore.preference.(*SqlPreferenceStore).CreateIndexesIfNotExists()
|
||||
|
||||
if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 {
|
||||
if model.IsPreviousVersion(schemaVersion) || isSchemaVersion07 || isSchemaVersion10 {
|
||||
sqlStore.system.Update(&model.System{Name: "Version", Value: model.CurrentVersion})
|
||||
l4g.Warn("The database schema has been upgraded to version " + model.CurrentVersion)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore {
|
||||
}
|
||||
|
||||
func (s SqlTeamStore) UpgradeSchemaIfNeeded() {
|
||||
// REMOVE in 1.2
|
||||
// REMOVE AFTER 1.2 SHIP see PLT-828
|
||||
s.RemoveColumnIfExists("Teams", "AllowValet")
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
|
||||
}
|
||||
|
||||
func (us SqlUserStore) UpgradeSchemaIfNeeded() {
|
||||
// REMOVE in 1.2
|
||||
// REMOVE AFTER 1.2 SHIP see PLT-828
|
||||
us.CreateColumnIfNotExists("Users", "ThemeProps", "varchar(2000)", "character varying(2000)", "{}")
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ type ChannelStore interface {
|
||||
CheckPermissionsToByName(teamId string, channelName string, userId string) StoreChannel
|
||||
UpdateLastViewedAt(channelId string, userId string) StoreChannel
|
||||
IncrementMentionCount(channelId string, userId string) StoreChannel
|
||||
AnalyticsTypeCount(teamId string, channelType string) StoreChannel
|
||||
}
|
||||
|
||||
type PostStore interface {
|
||||
@@ -87,6 +88,9 @@ type PostStore interface {
|
||||
GetEtag(channelId string) StoreChannel
|
||||
Search(teamId string, userId string, params *model.SearchParams) StoreChannel
|
||||
GetForExport(channelId string) StoreChannel
|
||||
AnalyticsUserCountsWithPostsByDay(teamId string) StoreChannel
|
||||
AnalyticsPostCountsByDay(teamId string) StoreChannel
|
||||
AnalyticsPostCount(teamId string) StoreChannel
|
||||
}
|
||||
|
||||
type UserStore interface {
|
||||
|
||||
@@ -159,6 +159,13 @@ func LoadConfig(fileName string) {
|
||||
configureLog(&config.LogSettings)
|
||||
TestConnection(&config)
|
||||
|
||||
if config.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
|
||||
dir := config.FileSettings.Directory
|
||||
if len(dir) > 0 && dir[len(dir)-1:] != "/" {
|
||||
config.FileSettings.Directory += "/"
|
||||
}
|
||||
}
|
||||
|
||||
Cfg = &config
|
||||
SanitizeOptions = getSanitizeOptions(Cfg)
|
||||
ClientCfg = getClientConfig(Cfg)
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
"globals": {
|
||||
"React": false,
|
||||
"ReactDOM": false,
|
||||
"ReactBootstrap": false
|
||||
"ReactBootstrap": false,
|
||||
"Chart": false
|
||||
},
|
||||
"rules": {
|
||||
"comma-dangle": [2, "never"],
|
||||
|
||||
@@ -18,6 +18,7 @@ var SqlSettingsTab = require('./sql_settings.jsx');
|
||||
var TeamSettingsTab = require('./team_settings.jsx');
|
||||
var ServiceSettingsTab = require('./service_settings.jsx');
|
||||
var TeamUsersTab = require('./team_users.jsx');
|
||||
var TeamAnalyticsTab = require('./team_analytics.jsx');
|
||||
|
||||
export default class AdminController extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -149,6 +150,10 @@ export default class AdminController extends React.Component {
|
||||
if (this.state.teams) {
|
||||
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;
|
||||
}
|
||||
} else if (this.state.selected === 'team_analytics') {
|
||||
if (this.state.teams) {
|
||||
tab = <TeamAnalyticsTab team={this.state.teams[this.state.selectedTeam]} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ export default class AdminSidebar extends React.Component {
|
||||
handleClick(name, teamId, e) {
|
||||
e.preventDefault();
|
||||
this.props.selectTab(name, teamId);
|
||||
history.pushState({name: name, teamId: teamId}, null, `/admin_console/${name}/${teamId || ''}`);
|
||||
history.pushState({name, teamId}, null, `/admin_console/${name}/${teamId || ''}`);
|
||||
}
|
||||
|
||||
isSelected(name, teamId) {
|
||||
@@ -121,6 +121,15 @@ export default class AdminSidebar extends React.Component {
|
||||
{'- Users'}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href='#'
|
||||
className={this.isSelected('team_analytics', team.id)}
|
||||
onClick={this.handleClick.bind(this, 'team_analytics', team.id)}
|
||||
>
|
||||
{'- Statistics'}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
50
web/react/components/admin_console/line_chart.jsx
Обычный файл
50
web/react/components/admin_console/line_chart.jsx
Обычный файл
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
export default class LineChart extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.initChart = this.initChart.bind(this);
|
||||
this.chart = null;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.initChart(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
this.initChart(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.chart) {
|
||||
this.chart.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
initChart(props) {
|
||||
var el = ReactDOM.findDOMNode(this);
|
||||
var ctx = el.getContext('2d');
|
||||
this.chart = new Chart(ctx).Line(props.data, props.options || {}); //eslint-disable-line new-cap
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<canvas
|
||||
width={this.props.width}
|
||||
height={this.props.height}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
LineChart.propTypes = {
|
||||
width: React.PropTypes.string,
|
||||
height: React.PropTypes.string,
|
||||
data: React.PropTypes.object,
|
||||
options: React.PropTypes.object
|
||||
};
|
||||
353
web/react/components/admin_console/team_analytics.jsx
Обычный файл
353
web/react/components/admin_console/team_analytics.jsx
Обычный файл
@@ -0,0 +1,353 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var Client = require('../../utils/client.jsx');
|
||||
var Utils = require('../../utils/utils.jsx');
|
||||
var LineChart = require('./line_chart.jsx');
|
||||
|
||||
export default class TeamAnalytics extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.getData = this.getData.bind(this);
|
||||
|
||||
this.state = {
|
||||
users: null,
|
||||
serverError: null,
|
||||
channel_open_count: null,
|
||||
channel_private_count: null,
|
||||
post_count: null,
|
||||
post_counts_day: null,
|
||||
user_counts_with_posts_day: null,
|
||||
recent_active_users: null,
|
||||
newly_created_users: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.getData(this.props.team.id);
|
||||
}
|
||||
|
||||
getData(teamId) {
|
||||
Client.getAnalytics(
|
||||
teamId,
|
||||
'standard',
|
||||
(data) => {
|
||||
for (var index in data) {
|
||||
if (data[index].name === 'channel_open_count') {
|
||||
this.setState({channel_open_count: data[index].value});
|
||||
}
|
||||
|
||||
if (data[index].name === 'channel_private_count') {
|
||||
this.setState({channel_private_count: data[index].value});
|
||||
}
|
||||
|
||||
if (data[index].name === 'post_count') {
|
||||
this.setState({post_count: data[index].value});
|
||||
}
|
||||
}
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
|
||||
Client.getAnalytics(
|
||||
teamId,
|
||||
'post_counts_day',
|
||||
(data) => {
|
||||
var chartData = {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Total Posts',
|
||||
fillColor: 'rgba(151,187,205,0.2)',
|
||||
strokeColor: 'rgba(151,187,205,1)',
|
||||
pointColor: 'rgba(151,187,205,1)',
|
||||
pointStrokeColor: '#fff',
|
||||
pointHighlightFill: '#fff',
|
||||
pointHighlightStroke: 'rgba(151,187,205,1)',
|
||||
data: []
|
||||
}]
|
||||
};
|
||||
|
||||
for (var index in data) {
|
||||
if (data[index]) {
|
||||
var row = data[index];
|
||||
chartData.labels.push(row.name);
|
||||
chartData.datasets[0].data.push(row.value);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({post_counts_day: chartData});
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
|
||||
Client.getAnalytics(
|
||||
teamId,
|
||||
'user_counts_with_posts_day',
|
||||
(data) => {
|
||||
var chartData = {
|
||||
labels: [],
|
||||
datasets: [{
|
||||
label: 'Active Users With Posts',
|
||||
fillColor: 'rgba(151,187,205,0.2)',
|
||||
strokeColor: 'rgba(151,187,205,1)',
|
||||
pointColor: 'rgba(151,187,205,1)',
|
||||
pointStrokeColor: '#fff',
|
||||
pointHighlightFill: '#fff',
|
||||
pointHighlightStroke: 'rgba(151,187,205,1)',
|
||||
data: []
|
||||
}]
|
||||
};
|
||||
|
||||
for (var index in data) {
|
||||
if (data[index]) {
|
||||
var row = data[index];
|
||||
chartData.labels.push(row.name);
|
||||
chartData.datasets[0].data.push(row.value);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({user_counts_with_posts_day: chartData});
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
|
||||
Client.getProfilesForTeam(
|
||||
teamId,
|
||||
(users) => {
|
||||
this.setState({users});
|
||||
|
||||
var usersList = [];
|
||||
for (var id in users) {
|
||||
if (users.hasOwnProperty(id)) {
|
||||
usersList.push(users[id]);
|
||||
}
|
||||
}
|
||||
|
||||
usersList.sort((a, b) => {
|
||||
if (a.last_activity_at < b.last_activity_at) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.last_activity_at > b.last_activity_at) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
var recentActive = [];
|
||||
for (let i = 0; i < usersList.length; i++) {
|
||||
recentActive.push(usersList[i]);
|
||||
if (i > 19) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({recent_active_users: recentActive});
|
||||
|
||||
usersList.sort((a, b) => {
|
||||
if (a.create_at < b.create_at) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (a.create_at > b.create_at) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
|
||||
var newlyCreated = [];
|
||||
for (let i = 0; i < usersList.length; i++) {
|
||||
newlyCreated.push(usersList[i]);
|
||||
if (i > 19) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({newly_created_users: newlyCreated});
|
||||
},
|
||||
(err) => {
|
||||
this.setState({serverError: err.message});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
this.setState({
|
||||
users: null,
|
||||
serverError: null,
|
||||
channel_open_count: null,
|
||||
channel_private_count: null,
|
||||
post_count: null,
|
||||
post_counts_day: null,
|
||||
user_counts_with_posts_day: null,
|
||||
recent_active_users: null,
|
||||
newly_created_users: null
|
||||
});
|
||||
|
||||
this.getData(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>;
|
||||
}
|
||||
|
||||
var totalCount = (
|
||||
<div className='total-count text-center'>
|
||||
<div>{'Total Users'}</div>
|
||||
<div>{this.state.users == null ? 'Loading...' : Object.keys(this.state.users).length}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var openChannelCount = (
|
||||
<div className='total-count text-center'>
|
||||
<div>{'Public Groups'}</div>
|
||||
<div>{this.state.channel_open_count == null ? 'Loading...' : this.state.channel_open_count}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var openPrivateCount = (
|
||||
<div className='total-count text-center'>
|
||||
<div>{'Private Groups'}</div>
|
||||
<div>{this.state.channel_private_count == null ? 'Loading...' : this.state.channel_private_count}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var postCount = (
|
||||
<div className='total-count text-center'>
|
||||
<div>{'Total Posts'}</div>
|
||||
<div>{this.state.post_count == null ? 'Loading...' : this.state.post_count}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
var postCountsByDay = (
|
||||
<div className='total-count-by-day'>
|
||||
<div>{'Total Posts'}</div>
|
||||
<div>{'Loading...'}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.post_counts_day != null) {
|
||||
postCountsByDay = (
|
||||
<div className='total-count-by-day'>
|
||||
<div>{'Total Posts'}</div>
|
||||
<LineChart
|
||||
data={this.state.post_counts_day}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var usersWithPostsByDay = (
|
||||
<div className='total-count-by-day'>
|
||||
<div>{'Total Posts'}</div>
|
||||
<div>{'Loading...'}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.user_counts_with_posts_day != null) {
|
||||
usersWithPostsByDay = (
|
||||
<div className='total-count-by-day'>
|
||||
<div>{'Active Users With Posts'}</div>
|
||||
<LineChart
|
||||
data={this.state.user_counts_with_posts_day}
|
||||
width='740'
|
||||
height='225'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var recentActiveUser = (
|
||||
<div className='recent-active-users'>
|
||||
<div>{'Recent Active Users'}</div>
|
||||
<div>{'Loading...'}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.recent_active_users != null) {
|
||||
recentActiveUser = (
|
||||
<div className='recent-active-users'>
|
||||
<div>{'Recent Active Users'}</div>
|
||||
<table width='90%'>
|
||||
<tbody>
|
||||
{
|
||||
this.state.recent_active_users.map((user) => {
|
||||
return (
|
||||
<tr key={user.id}>
|
||||
<td className='recent-active-users-td'>{user.email}</td>
|
||||
<td className='recent-active-users-td'>{Utils.displayDateTime(user.last_activity_at)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
var newUsers = (
|
||||
<div className='recent-active-users'>
|
||||
<div>{'Newly Created Users'}</div>
|
||||
<div>{'Loading...'}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (this.state.newly_created_users != null) {
|
||||
newUsers = (
|
||||
<div className='recent-active-users'>
|
||||
<div>{'Newly Created Users'}</div>
|
||||
<table width='90%'>
|
||||
<tbody>
|
||||
{
|
||||
this.state.newly_created_users.map((user) => {
|
||||
return (
|
||||
<tr key={user.id}>
|
||||
<td className='recent-active-users-td'>{user.email}</td>
|
||||
<td className='recent-active-users-td'>{Utils.displayDateTime(user.create_at)}</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='wrapper--fixed'>
|
||||
<h2>{'Statistics for ' + this.props.team.name}</h2>
|
||||
{serverError}
|
||||
{totalCount}
|
||||
{postCount}
|
||||
{openChannelCount}
|
||||
{openPrivateCount}
|
||||
{postCountsByDay}
|
||||
{usersWithPostsByDay}
|
||||
{recentActiveUser}
|
||||
{newUsers}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamAnalytics.propTypes = {
|
||||
team: React.PropTypes.object
|
||||
};
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
const ChannelStore = require('../stores/channel_store.jsx');
|
||||
const UserStore = require('../stores/user_store.jsx');
|
||||
const PostStore = require('../stores/post_store.jsx');
|
||||
const SearchStore = require('../stores/search_store.jsx');
|
||||
const NavbarSearchBox = require('./search_bar.jsx');
|
||||
const AsyncClient = require('../utils/async_client.jsx');
|
||||
const Client = require('../utils/client.jsx');
|
||||
@@ -35,19 +35,19 @@ export default class ChannelHeader extends React.Component {
|
||||
memberChannel: ChannelStore.getCurrentMember(),
|
||||
memberTeam: UserStore.getCurrentUser(),
|
||||
users: ChannelStore.getCurrentExtraInfo().members,
|
||||
searchVisible: PostStore.getSearchResults() !== null
|
||||
searchVisible: SearchStore.getSearchResults() !== null
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
ChannelStore.addChangeListener(this.onListenerChange);
|
||||
ChannelStore.addExtraInfoChangeListener(this.onListenerChange);
|
||||
PostStore.addSearchChangeListener(this.onListenerChange);
|
||||
SearchStore.addSearchChangeListener(this.onListenerChange);
|
||||
UserStore.addChangeListener(this.onListenerChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ChannelStore.removeChangeListener(this.onListenerChange);
|
||||
ChannelStore.removeExtraInfoChangeListener(this.onListenerChange);
|
||||
PostStore.removeSearchChangeListener(this.onListenerChange);
|
||||
SearchStore.removeSearchChangeListener(this.onListenerChange);
|
||||
UserStore.addChangeListener(this.onListenerChange);
|
||||
}
|
||||
onListenerChange() {
|
||||
|
||||
@@ -253,8 +253,14 @@ export default class CreatePost extends React.Component {
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, previews: draft.previews});
|
||||
}
|
||||
handleUploadError(err, clientId) {
|
||||
let message = err;
|
||||
if (message && typeof message !== 'string') {
|
||||
// err is an AppError from the server
|
||||
message = err.message;
|
||||
}
|
||||
|
||||
if (clientId === -1) {
|
||||
this.setState({serverError: err});
|
||||
this.setState({serverError: message});
|
||||
} else {
|
||||
const draft = PostStore.getDraft(this.state.channelId);
|
||||
|
||||
@@ -265,7 +271,7 @@ export default class CreatePost extends React.Component {
|
||||
|
||||
PostStore.storeDraft(this.state.channelId, draft);
|
||||
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: err});
|
||||
this.setState({uploadsInProgress: draft.uploadsInProgress, serverError: message});
|
||||
}
|
||||
}
|
||||
handleTextDrop(text) {
|
||||
|
||||
@@ -9,12 +9,8 @@ export default class ErrorBar extends React.Component {
|
||||
|
||||
this.onErrorChange = this.onErrorChange.bind(this);
|
||||
this.handleClose = this.handleClose.bind(this);
|
||||
this.prevTimer = null;
|
||||
|
||||
this.state = ErrorStore.getLastError();
|
||||
if (this.isValidError(this.state)) {
|
||||
this.prevTimer = setTimeout(this.handleClose, 10000);
|
||||
}
|
||||
}
|
||||
|
||||
isValidError(s) {
|
||||
@@ -56,16 +52,8 @@ export default class ErrorBar extends React.Component {
|
||||
onErrorChange() {
|
||||
var newState = ErrorStore.getLastError();
|
||||
|
||||
if (this.prevTimer != null) {
|
||||
clearInterval(this.prevTimer);
|
||||
this.prevTimer = null;
|
||||
}
|
||||
|
||||
if (newState) {
|
||||
this.setState(newState);
|
||||
if (!this.isConnectionError(newState)) {
|
||||
this.prevTimer = setTimeout(this.handleClose, 10000);
|
||||
}
|
||||
} else {
|
||||
this.setState({message: null});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var SearchStore = require('../stores/search_store.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var Mention = require('./mention.jsx');
|
||||
|
||||
@@ -66,7 +66,7 @@ export default class MentionList extends React.Component {
|
||||
}
|
||||
}
|
||||
componentDidMount() {
|
||||
PostStore.addMentionDataChangeListener(this.onListenerChange);
|
||||
SearchStore.addMentionDataChangeListener(this.onListenerChange);
|
||||
|
||||
$('.post-right__scroll').scroll(this.onScroll);
|
||||
|
||||
@@ -74,7 +74,7 @@ export default class MentionList extends React.Component {
|
||||
$(document).click(this.onClick);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
PostStore.removeMentionDataChangeListener(this.onListenerChange);
|
||||
SearchStore.removeMentionDataChangeListener(this.onListenerChange);
|
||||
$('body').off('keydown.mentionlist', '#' + this.props.id);
|
||||
}
|
||||
|
||||
@@ -217,12 +217,17 @@ export default class MentionList extends React.Component {
|
||||
if (this.state.selectedMention === index) {
|
||||
isFocused = 'mentions-focus';
|
||||
}
|
||||
|
||||
if (!users[i].secondary_text) {
|
||||
users[i].secondary_text = Utils.getFullName(users[i]);
|
||||
}
|
||||
|
||||
mentions[index] = (
|
||||
<Mention
|
||||
key={'mention_key_' + index}
|
||||
ref={'mention' + index}
|
||||
username={users[i].username}
|
||||
secondary_text={Utils.getFullName(users[i])}
|
||||
secondary_text={users[i].secondary_text}
|
||||
id={users[i].id}
|
||||
listId={index}
|
||||
isFocused={isFocused}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
const AsyncClient = require('../utils/async_client.jsx');
|
||||
const ChannelStore = require('../stores/channel_store.jsx');
|
||||
const Constants = require('../utils/constants.jsx');
|
||||
const Client = require('../utils/client.jsx');
|
||||
const Modal = ReactBootstrap.Modal;
|
||||
const PreferenceStore = require('../stores/preference_store.jsx');
|
||||
const TeamStore = require('../stores/team_store.jsx');
|
||||
const UserStore = require('../stores/user_store.jsx');
|
||||
const Utils = require('../utils/utils.jsx');
|
||||
|
||||
@@ -70,52 +64,24 @@ export default class MoreDirectChannels extends React.Component {
|
||||
}
|
||||
|
||||
handleShowDirectChannel(teammate, e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.loadingDMChannel !== -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), teammate.id);
|
||||
let channel = ChannelStore.getByName(channelName);
|
||||
|
||||
const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, teammate.id, 'true');
|
||||
AsyncClient.savePreferences([preference]);
|
||||
|
||||
if (channel) {
|
||||
Utils.switchChannel(channel);
|
||||
|
||||
this.handleHide();
|
||||
} else {
|
||||
this.setState({loadingDMChannel: teammate.id});
|
||||
|
||||
channel = {
|
||||
name: channelName,
|
||||
last_post_at: 0,
|
||||
total_msg_count: 0,
|
||||
type: 'D',
|
||||
display_name: teammate.username,
|
||||
teammate_id: teammate.id,
|
||||
status: UserStore.getStatus(teammate.id)
|
||||
};
|
||||
|
||||
Client.createDirectChannel(
|
||||
channel,
|
||||
teammate.id,
|
||||
(data) => {
|
||||
this.setState({loadingDMChannel: -1});
|
||||
|
||||
AsyncClient.getChannel(data.id);
|
||||
Utils.switchChannel(data);
|
||||
|
||||
this.handleHide();
|
||||
},
|
||||
() => {
|
||||
this.setState({loadingDMChannel: -1});
|
||||
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
|
||||
}
|
||||
);
|
||||
}
|
||||
this.setState({loadingDMChannel: teammate.id});
|
||||
Utils.openDirectChannelToUser(
|
||||
teammate,
|
||||
(channel) => {
|
||||
Utils.switchChannel(channel);
|
||||
this.setState({loadingDMChannel: -1});
|
||||
this.handleHide();
|
||||
},
|
||||
() => {
|
||||
this.setState({loadingDMChannel: -1});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleUserChange() {
|
||||
|
||||
@@ -3,9 +3,23 @@
|
||||
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var Popover = ReactBootstrap.Popover;
|
||||
var OverlayTrigger = ReactBootstrap.OverlayTrigger;
|
||||
var Overlay = ReactBootstrap.Overlay;
|
||||
const Utils = require('../utils/utils.jsx');
|
||||
|
||||
const ChannelStore = require('../stores/channel_store.jsx');
|
||||
|
||||
export default class PopoverListMembers extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
|
||||
this.closePopover = this.closePopover.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({showPopover: false});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const originalLeave = $.fn.popover.Constructor.prototype.leave;
|
||||
$.fn.popover.Constructor.prototype.leave = function onLeave(obj) {
|
||||
@@ -27,12 +41,36 @@ export default class PopoverListMembers extends React.Component {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
handleShowDirectChannel(teammate, e) {
|
||||
e.preventDefault();
|
||||
|
||||
Utils.openDirectChannelToUser(
|
||||
teammate,
|
||||
(channel, channelAlreadyExisted) => {
|
||||
Utils.switchChannel(channel);
|
||||
if (channelAlreadyExisted) {
|
||||
this.closePopover();
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.closePopover();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
closePopover() {
|
||||
this.setState({showPopover: false});
|
||||
}
|
||||
|
||||
render() {
|
||||
let popoverHtml = [];
|
||||
let count = 0;
|
||||
let countText = '-';
|
||||
const members = this.props.members;
|
||||
const teamMembers = UserStore.getProfilesUsernameMap();
|
||||
const currentUserId = UserStore.getCurrentId();
|
||||
const ch = ChannelStore.getCurrent();
|
||||
|
||||
if (members && teamMembers) {
|
||||
members.sort((a, b) => {
|
||||
@@ -40,13 +78,74 @@ export default class PopoverListMembers extends React.Component {
|
||||
});
|
||||
|
||||
members.forEach((m, i) => {
|
||||
const details = [];
|
||||
|
||||
const fullName = Utils.getFullName(m);
|
||||
if (fullName) {
|
||||
details.push(
|
||||
<span
|
||||
key={`${m.id}__full-name`}
|
||||
className='full-name'
|
||||
>
|
||||
{fullName}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (m.nickname) {
|
||||
const separator = fullName ? ' - ' : '';
|
||||
details.push(
|
||||
<span
|
||||
key={`${m.nickname}__nickname`}
|
||||
>
|
||||
{separator + m.nickname}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
let button = '';
|
||||
if (currentUserId !== m.id && ch.type !== 'D') {
|
||||
button = (
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-primary btn-message'
|
||||
onClick={(e) => this.handleShowDirectChannel(m, e)}
|
||||
>
|
||||
{'Message'}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (teamMembers[m.username] && teamMembers[m.username].delete_at <= 0) {
|
||||
popoverHtml.push(
|
||||
<div
|
||||
className='text--nowrap'
|
||||
key={'popover-member-' + i}
|
||||
>
|
||||
{m.username}
|
||||
|
||||
<img
|
||||
className='profile-img pull-left'
|
||||
width='38'
|
||||
height='38'
|
||||
src={`/api/v1/users/${m.id}/image?time=${m.update_at}&${Utils.getSessionIndex()}`}
|
||||
/>
|
||||
<div className='pull-left'>
|
||||
<div
|
||||
className='more-name'
|
||||
>
|
||||
{m.username}
|
||||
</div>
|
||||
<div
|
||||
className='more-description'
|
||||
>
|
||||
{details}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className='pull-right profile-action'
|
||||
>
|
||||
{button}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
count++;
|
||||
@@ -61,29 +160,37 @@ export default class PopoverListMembers extends React.Component {
|
||||
}
|
||||
|
||||
return (
|
||||
<OverlayTrigger
|
||||
trigger='click'
|
||||
placement='bottom'
|
||||
rootClose={true}
|
||||
overlay={
|
||||
<div>
|
||||
<div
|
||||
id='member_popover'
|
||||
ref='member_popover_target'
|
||||
onClick={(e) => this.setState({popoverTarget: e.target, showPopover: !this.state.showPopover})}
|
||||
>
|
||||
<div>
|
||||
{countText}
|
||||
<span
|
||||
className='fa fa-user'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Overlay
|
||||
rootClose={true}
|
||||
onHide={this.closePopover}
|
||||
show={this.state.showPopover}
|
||||
target={() => this.state.popoverTarget}
|
||||
placement='bottom'
|
||||
>
|
||||
<Popover
|
||||
title='Members'
|
||||
id='member-list-popover'
|
||||
>
|
||||
{popoverHtml}
|
||||
<div>
|
||||
{popoverHtml}
|
||||
</div>
|
||||
</Popover>
|
||||
}
|
||||
>
|
||||
<div id='member_popover'>
|
||||
<div>
|
||||
{countText}
|
||||
<span
|
||||
className='fa fa-user'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</div>
|
||||
</Overlay>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
var client = require('../utils/client.jsx');
|
||||
var AsyncClient = require('../utils/async_client.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var SearchStore = require('../stores/search_store.jsx');
|
||||
var AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
var utils = require('../utils/utils.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
@@ -30,17 +30,17 @@ export default class SearchBar extends React.Component {
|
||||
this.state = state;
|
||||
}
|
||||
getSearchTermStateFromStores() {
|
||||
var term = PostStore.getSearchTerm() || '';
|
||||
var term = SearchStore.getSearchTerm() || '';
|
||||
return {
|
||||
searchTerm: term
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
PostStore.addSearchTermChangeListener(this.onListenerChange);
|
||||
SearchStore.addSearchTermChangeListener(this.onListenerChange);
|
||||
this.mounted = true;
|
||||
}
|
||||
componentWillUnmount() {
|
||||
PostStore.removeSearchTermChangeListener(this.onListenerChange);
|
||||
SearchStore.removeSearchTermChangeListener(this.onListenerChange);
|
||||
this.mounted = false;
|
||||
}
|
||||
onListenerChange(doSearch, isMentionSearch) {
|
||||
@@ -84,8 +84,8 @@ export default class SearchBar extends React.Component {
|
||||
}
|
||||
handleUserInput(e) {
|
||||
var term = e.target.value;
|
||||
PostStore.storeSearchTerm(term);
|
||||
PostStore.emitSearchTermChange(false);
|
||||
SearchStore.storeSearchTerm(term);
|
||||
SearchStore.emitSearchTermChange(false);
|
||||
this.setState({searchTerm: term});
|
||||
|
||||
this.refs.autocomplete.handleInputChange(e.target, term);
|
||||
@@ -150,8 +150,8 @@ export default class SearchBar extends React.Component {
|
||||
textbox.value = text;
|
||||
utils.setCaretPosition(textbox, preText.length + word.length);
|
||||
|
||||
PostStore.storeSearchTerm(text);
|
||||
PostStore.emitSearchTermChange(false);
|
||||
SearchStore.storeSearchTerm(text);
|
||||
SearchStore.emitSearchTermChange(false);
|
||||
this.setState({searchTerm: text});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var SearchStore = require('../stores/search_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var SearchBox = require('./search_bar.jsx');
|
||||
var Utils = require('../utils/utils.jsx');
|
||||
@@ -9,7 +9,7 @@ var SearchResultsHeader = require('./search_results_header.jsx');
|
||||
var SearchResultsItem = require('./search_results_item.jsx');
|
||||
|
||||
function getStateFromStores() {
|
||||
return {results: PostStore.getSearchResults()};
|
||||
return {results: SearchStore.getSearchResults()};
|
||||
}
|
||||
|
||||
export default class SearchResults extends React.Component {
|
||||
@@ -30,7 +30,7 @@ export default class SearchResults extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
PostStore.addSearchChangeListener(this.onChange);
|
||||
SearchStore.addSearchChangeListener(this.onChange);
|
||||
this.resize();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export default class SearchResults extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
PostStore.removeSearchChangeListener(this.onChange);
|
||||
SearchStore.removeSearchChangeListener(this.onChange);
|
||||
this.mounted = false;
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export default class SearchResults extends React.Component {
|
||||
searchForm = <SearchBox />;
|
||||
}
|
||||
var noResults = (!results || !results.order || !results.order.length);
|
||||
var searchTerm = PostStore.getSearchTerm();
|
||||
var searchTerm = SearchStore.getSearchTerm();
|
||||
|
||||
var ctls = null;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var SearchStore = require('../stores/search_store.jsx');
|
||||
var ChannelStore = require('../stores/channel_store.jsx');
|
||||
var UserStore = require('../stores/user_store.jsx');
|
||||
var UserProfile = require('./user_profile.jsx');
|
||||
@@ -32,7 +32,7 @@ export default class SearchResultsItem extends React.Component {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECIEVED_POST_SELECTED,
|
||||
post_list: data,
|
||||
from_search: PostStore.getSearchTerm()
|
||||
from_search: SearchStore.getSearchTerm()
|
||||
});
|
||||
|
||||
AppDispatcher.handleServerAction({
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
|
||||
var SearchResults = require('./search_results.jsx');
|
||||
var RhsThread = require('./rhs_thread.jsx');
|
||||
var SearchStore = require('../stores/search_store.jsx');
|
||||
var PostStore = require('../stores/post_store.jsx');
|
||||
var Utils = require('../utils/utils.jsx');
|
||||
|
||||
function getStateFromStores() {
|
||||
return {search_visible: PostStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: PostStore.getIsMentionSearch()};
|
||||
return {search_visible: SearchStore.getSearchResults() != null, post_right_visible: PostStore.getSelectedPost() != null, is_mention_search: SearchStore.getIsMentionSearch()};
|
||||
}
|
||||
|
||||
export default class SidebarRight extends React.Component {
|
||||
@@ -22,11 +23,11 @@ export default class SidebarRight extends React.Component {
|
||||
this.state = getStateFromStores();
|
||||
}
|
||||
componentDidMount() {
|
||||
PostStore.addSearchChangeListener(this.onSearchChange);
|
||||
SearchStore.addSearchChangeListener(this.onSearchChange);
|
||||
PostStore.addSelectedPostChangeListener(this.onSelectedChange);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
PostStore.removeSearchChangeListener(this.onSearchChange);
|
||||
SearchStore.removeSearchChangeListener(this.onSearchChange);
|
||||
PostStore.removeSelectedPostChangeListener(this.onSelectedChange);
|
||||
}
|
||||
componentDidUpdate() {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// See License.txt for license information.
|
||||
|
||||
const AppDispatcher = require('../dispatcher/app_dispatcher.jsx');
|
||||
const PostStore = require('../stores/post_store.jsx');
|
||||
const SearchStore = require('../stores/search_store.jsx');
|
||||
const CommandList = require('./command_list.jsx');
|
||||
const ErrorStore = require('../stores/error_store.jsx');
|
||||
|
||||
@@ -54,7 +54,7 @@ export default class Textbox extends React.Component {
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
PostStore.addAddMentionListener(this.onListenerChange);
|
||||
SearchStore.addAddMentionListener(this.onListenerChange);
|
||||
ErrorStore.addChangeListener(this.onRecievedError);
|
||||
|
||||
this.resize();
|
||||
@@ -62,7 +62,7 @@ export default class Textbox extends React.Component {
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
PostStore.removeAddMentionListener(this.onListenerChange);
|
||||
SearchStore.removeAddMentionListener(this.onListenerChange);
|
||||
ErrorStore.removeChangeListener(this.onRecievedError);
|
||||
}
|
||||
|
||||
|
||||
@@ -171,7 +171,7 @@ export default class UserSettingsGeneralTab extends React.Component {
|
||||
}.bind(this),
|
||||
function imageUploadFailure(err) {
|
||||
var state = this.setupInitialState(this.props);
|
||||
state.serverError = err;
|
||||
state.serverError = err.message;
|
||||
this.setState(state);
|
||||
}.bind(this)
|
||||
);
|
||||
|
||||
@@ -12,11 +12,7 @@ var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
|
||||
var CHANGE_EVENT = 'change';
|
||||
var SEARCH_CHANGE_EVENT = 'search_change';
|
||||
var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
|
||||
var SELECTED_POST_CHANGE_EVENT = 'selected_post_change';
|
||||
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
|
||||
var ADD_MENTION_EVENT = 'add_mention';
|
||||
var EDIT_POST_EVENT = 'edit_post';
|
||||
|
||||
class PostStoreClass extends EventEmitter {
|
||||
@@ -26,21 +22,15 @@ class PostStoreClass extends EventEmitter {
|
||||
this.emitChange = this.emitChange.bind(this);
|
||||
this.addChangeListener = this.addChangeListener.bind(this);
|
||||
this.removeChangeListener = this.removeChangeListener.bind(this);
|
||||
this.emitSearchChange = this.emitSearchChange.bind(this);
|
||||
this.addSearchChangeListener = this.addSearchChangeListener.bind(this);
|
||||
this.removeSearchChangeListener = this.removeSearchChangeListener.bind(this);
|
||||
this.emitSearchTermChange = this.emitSearchTermChange.bind(this);
|
||||
this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this);
|
||||
this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this);
|
||||
|
||||
this.emitSelectedPostChange = this.emitSelectedPostChange.bind(this);
|
||||
this.addSelectedPostChangeListener = this.addSelectedPostChangeListener.bind(this);
|
||||
this.removeSelectedPostChangeListener = this.removeSelectedPostChangeListener.bind(this);
|
||||
this.emitMentionDataChange = this.emitMentionDataChange.bind(this);
|
||||
this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this);
|
||||
this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.bind(this);
|
||||
this.emitAddMention = this.emitAddMention.bind(this);
|
||||
this.addAddMentionListener = this.addAddMentionListener.bind(this);
|
||||
this.removeAddMentionListener = this.removeAddMentionListener.bind(this);
|
||||
|
||||
this.emitEditPost = this.emitEditPost.bind(this);
|
||||
this.addEditPostListener = this.addEditPostListener.bind(this);
|
||||
this.removeEditPostListener = this.removeEditPostListener.bind(this);
|
||||
|
||||
this.getCurrentPosts = this.getCurrentPosts.bind(this);
|
||||
this.storePosts = this.storePosts.bind(this);
|
||||
this.pStorePosts = this.pStorePosts.bind(this);
|
||||
@@ -59,13 +49,8 @@ class PostStoreClass extends EventEmitter {
|
||||
this.pRemovePendingPost = this.pRemovePendingPost.bind(this);
|
||||
this.clearPendingPosts = this.clearPendingPosts.bind(this);
|
||||
this.updatePendingPost = this.updatePendingPost.bind(this);
|
||||
this.storeSearchResults = this.storeSearchResults.bind(this);
|
||||
this.getSearchResults = this.getSearchResults.bind(this);
|
||||
this.getIsMentionSearch = this.getIsMentionSearch.bind(this);
|
||||
this.storeSelectedPost = this.storeSelectedPost.bind(this);
|
||||
this.getSelectedPost = this.getSelectedPost.bind(this);
|
||||
this.storeSearchTerm = this.storeSearchTerm.bind(this);
|
||||
this.getSearchTerm = this.getSearchTerm.bind(this);
|
||||
this.getEmptyDraft = this.getEmptyDraft.bind(this);
|
||||
this.storeCurrentDraft = this.storeCurrentDraft.bind(this);
|
||||
this.getCurrentDraft = this.getCurrentDraft.bind(this);
|
||||
@@ -77,9 +62,6 @@ class PostStoreClass extends EventEmitter {
|
||||
this.clearCommentDraftUploads = this.clearCommentDraftUploads.bind(this);
|
||||
this.storeLatestUpdate = this.storeLatestUpdate.bind(this);
|
||||
this.getLatestUpdate = this.getLatestUpdate.bind(this);
|
||||
this.emitEditPost = this.emitEditPost.bind(this);
|
||||
this.addEditPostListener = this.addEditPostListener.bind(this);
|
||||
this.removeEditPostListener = this.removeEditPostListener.bind(this);
|
||||
this.getCurrentUsersLatestPost = this.getCurrentUsersLatestPost.bind(this);
|
||||
}
|
||||
emitChange() {
|
||||
@@ -94,30 +76,6 @@ class PostStoreClass extends EventEmitter {
|
||||
this.removeListener(CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitSearchChange() {
|
||||
this.emit(SEARCH_CHANGE_EVENT);
|
||||
}
|
||||
|
||||
addSearchChangeListener(callback) {
|
||||
this.on(SEARCH_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeSearchChangeListener(callback) {
|
||||
this.removeListener(SEARCH_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitSearchTermChange(doSearch, isMentionSearch) {
|
||||
this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch);
|
||||
}
|
||||
|
||||
addSearchTermChangeListener(callback) {
|
||||
this.on(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeSearchTermChangeListener(callback) {
|
||||
this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitSelectedPostChange(fromSearch) {
|
||||
this.emit(SELECTED_POST_CHANGE_EVENT, fromSearch);
|
||||
}
|
||||
@@ -130,30 +88,6 @@ class PostStoreClass extends EventEmitter {
|
||||
this.removeListener(SELECTED_POST_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitMentionDataChange(id, mentionText) {
|
||||
this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText);
|
||||
}
|
||||
|
||||
addMentionDataChangeListener(callback) {
|
||||
this.on(MENTION_DATA_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeMentionDataChangeListener(callback) {
|
||||
this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitAddMention(id, username) {
|
||||
this.emit(ADD_MENTION_EVENT, id, username);
|
||||
}
|
||||
|
||||
addAddMentionListener(callback) {
|
||||
this.on(ADD_MENTION_EVENT, callback);
|
||||
}
|
||||
|
||||
removeAddMentionListener(callback) {
|
||||
this.removeListener(ADD_MENTION_EVENT, callback);
|
||||
}
|
||||
|
||||
emitEditPost(post) {
|
||||
this.emit(EDIT_POST_EVENT, post);
|
||||
}
|
||||
@@ -181,9 +115,9 @@ class PostStoreClass extends EventEmitter {
|
||||
|
||||
var postList = makePostListNonNull(this.getPosts(channelId));
|
||||
|
||||
for (let pid in newPostList.posts) {
|
||||
for (const pid in newPostList.posts) {
|
||||
if (newPostList.posts.hasOwnProperty(pid)) {
|
||||
var np = newPostList.posts[pid];
|
||||
const np = newPostList.posts[pid];
|
||||
if (np.delete_at === 0) {
|
||||
postList.posts[pid] = np;
|
||||
if (postList.order.indexOf(pid) === -1) {
|
||||
@@ -194,7 +128,7 @@ class PostStoreClass extends EventEmitter {
|
||||
delete postList.posts[pid];
|
||||
}
|
||||
|
||||
var index = postList.order.indexOf(pid);
|
||||
const index = postList.order.indexOf(pid);
|
||||
if (index !== -1) {
|
||||
postList.order.splice(index, 1);
|
||||
}
|
||||
@@ -202,7 +136,7 @@ class PostStoreClass extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
postList.order.sort(function postSort(a, b) {
|
||||
postList.order.sort((a, b) => {
|
||||
if (postList.posts[a].create_at > postList.posts[b].create_at) {
|
||||
return -1;
|
||||
}
|
||||
@@ -306,7 +240,7 @@ class PostStoreClass extends EventEmitter {
|
||||
var posts = postList.posts;
|
||||
|
||||
// sort failed posts to the bottom
|
||||
postList.order.sort(function postSort(a, b) {
|
||||
postList.order.sort((a, b) => {
|
||||
if (posts[a].state === Constants.POST_LOADING && posts[b].state === Constants.POST_FAILED) {
|
||||
return 1;
|
||||
}
|
||||
@@ -371,7 +305,7 @@ class PostStoreClass extends EventEmitter {
|
||||
this.pStorePendingPosts(channelId, postList);
|
||||
}
|
||||
clearPendingPosts() {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', function clearPending(key) {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('pending_posts_', (key) => {
|
||||
BrowserStore.removeItem(key);
|
||||
});
|
||||
}
|
||||
@@ -387,28 +321,12 @@ class PostStoreClass extends EventEmitter {
|
||||
this.pStorePendingPosts(post.channel_id, postList);
|
||||
this.emitChange();
|
||||
}
|
||||
storeSearchResults(results, isMentionSearch) {
|
||||
BrowserStore.setItem('search_results', results);
|
||||
BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
|
||||
}
|
||||
getSearchResults() {
|
||||
return BrowserStore.getItem('search_results');
|
||||
}
|
||||
getIsMentionSearch() {
|
||||
return BrowserStore.getItem('is_mention_search');
|
||||
}
|
||||
storeSelectedPost(postList) {
|
||||
BrowserStore.setItem('select_post', postList);
|
||||
}
|
||||
getSelectedPost() {
|
||||
return BrowserStore.getItem('select_post');
|
||||
}
|
||||
storeSearchTerm(term) {
|
||||
BrowserStore.setItem('search_term', term);
|
||||
}
|
||||
getSearchTerm() {
|
||||
return BrowserStore.getItem('search_term');
|
||||
}
|
||||
getEmptyDraft() {
|
||||
return {message: '', uploadsInProgress: [], previews: []};
|
||||
}
|
||||
@@ -433,7 +351,7 @@ class PostStoreClass extends EventEmitter {
|
||||
return BrowserStore.getGlobalItem('comment_draft_' + parentPostId, this.getEmptyDraft());
|
||||
}
|
||||
clearDraftUploads() {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('draft_', function clearUploads(key, value) {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('draft_', (key, value) => {
|
||||
if (value) {
|
||||
value.uploadsInProgress = [];
|
||||
BrowserStore.setItem(key, value);
|
||||
@@ -441,7 +359,7 @@ class PostStoreClass extends EventEmitter {
|
||||
});
|
||||
}
|
||||
clearCommentDraftUploads() {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', function clearUploads(key, value) {
|
||||
BrowserStore.actionOnGlobalItemsWithPrefix('comment_draft_', (key, value) => {
|
||||
if (value) {
|
||||
value.uploadsInProgress = [];
|
||||
BrowserStore.setItem(key, value);
|
||||
@@ -458,7 +376,7 @@ class PostStoreClass extends EventEmitter {
|
||||
|
||||
var PostStore = new PostStoreClass();
|
||||
|
||||
PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
|
||||
PostStore.dispatchToken = AppDispatcher.register((payload) => {
|
||||
var action = payload.action;
|
||||
|
||||
switch (action.type) {
|
||||
@@ -469,24 +387,10 @@ PostStore.dispatchToken = AppDispatcher.register(function registry(payload) {
|
||||
PostStore.pStorePost(action.post);
|
||||
PostStore.emitChange();
|
||||
break;
|
||||
case ActionTypes.RECIEVED_SEARCH:
|
||||
PostStore.storeSearchResults(action.results, action.is_mention_search);
|
||||
PostStore.emitSearchChange();
|
||||
break;
|
||||
case ActionTypes.RECIEVED_SEARCH_TERM:
|
||||
PostStore.storeSearchTerm(action.term);
|
||||
PostStore.emitSearchTermChange(action.do_search, action.is_mention_search);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_POST_SELECTED:
|
||||
PostStore.storeSelectedPost(action.post_list);
|
||||
PostStore.emitSelectedPostChange(action.from_search);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_MENTION_DATA:
|
||||
PostStore.emitMentionDataChange(action.id, action.mention_text);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_ADD_MENTION:
|
||||
PostStore.emitAddMention(action.id, action.username);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_EDIT_POST:
|
||||
PostStore.emitEditPost(action);
|
||||
break;
|
||||
|
||||
153
web/react/stores/search_store.jsx
Обычный файл
153
web/react/stores/search_store.jsx
Обычный файл
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
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 CHANGE_EVENT = 'change';
|
||||
var SEARCH_CHANGE_EVENT = 'search_change';
|
||||
var SEARCH_TERM_CHANGE_EVENT = 'search_term_change';
|
||||
var MENTION_DATA_CHANGE_EVENT = 'mention_data_change';
|
||||
var ADD_MENTION_EVENT = 'add_mention';
|
||||
|
||||
class SearchStoreClass extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.emitChange = this.emitChange.bind(this);
|
||||
this.addChangeListener = this.addChangeListener.bind(this);
|
||||
this.removeChangeListener = this.removeChangeListener.bind(this);
|
||||
|
||||
this.emitSearchChange = this.emitSearchChange.bind(this);
|
||||
this.addSearchChangeListener = this.addSearchChangeListener.bind(this);
|
||||
this.removeSearchChangeListener = this.removeSearchChangeListener.bind(this);
|
||||
|
||||
this.emitSearchTermChange = this.emitSearchTermChange.bind(this);
|
||||
this.addSearchTermChangeListener = this.addSearchTermChangeListener.bind(this);
|
||||
this.removeSearchTermChangeListener = this.removeSearchTermChangeListener.bind(this);
|
||||
|
||||
this.emitMentionDataChange = this.emitMentionDataChange.bind(this);
|
||||
this.addMentionDataChangeListener = this.addMentionDataChangeListener.bind(this);
|
||||
this.removeMentionDataChangeListener = this.removeMentionDataChangeListener.bind(this);
|
||||
|
||||
this.getSearchResults = this.getSearchResults.bind(this);
|
||||
this.getIsMentionSearch = this.getIsMentionSearch.bind(this);
|
||||
|
||||
this.storeSearchTerm = this.storeSearchTerm.bind(this);
|
||||
this.getSearchTerm = this.getSearchTerm.bind(this);
|
||||
|
||||
this.storeSearchResults = this.storeSearchResults.bind(this);
|
||||
}
|
||||
|
||||
emitChange() {
|
||||
this.emit(CHANGE_EVENT);
|
||||
}
|
||||
|
||||
addChangeListener(callback) {
|
||||
this.on(CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeChangeListener(callback) {
|
||||
this.removeListener(CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitSearchChange() {
|
||||
this.emit(SEARCH_CHANGE_EVENT);
|
||||
}
|
||||
|
||||
addSearchChangeListener(callback) {
|
||||
this.on(SEARCH_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeSearchChangeListener(callback) {
|
||||
this.removeListener(SEARCH_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitSearchTermChange(doSearch, isMentionSearch) {
|
||||
this.emit(SEARCH_TERM_CHANGE_EVENT, doSearch, isMentionSearch);
|
||||
}
|
||||
|
||||
addSearchTermChangeListener(callback) {
|
||||
this.on(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeSearchTermChangeListener(callback) {
|
||||
this.removeListener(SEARCH_TERM_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
getSearchResults() {
|
||||
return BrowserStore.getItem('search_results');
|
||||
}
|
||||
|
||||
getIsMentionSearch() {
|
||||
return BrowserStore.getItem('is_mention_search');
|
||||
}
|
||||
|
||||
storeSearchTerm(term) {
|
||||
BrowserStore.setItem('search_term', term);
|
||||
}
|
||||
|
||||
getSearchTerm() {
|
||||
return BrowserStore.getItem('search_term');
|
||||
}
|
||||
|
||||
emitMentionDataChange(id, mentionText) {
|
||||
this.emit(MENTION_DATA_CHANGE_EVENT, id, mentionText);
|
||||
}
|
||||
|
||||
addMentionDataChangeListener(callback) {
|
||||
this.on(MENTION_DATA_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
removeMentionDataChangeListener(callback) {
|
||||
this.removeListener(MENTION_DATA_CHANGE_EVENT, callback);
|
||||
}
|
||||
|
||||
emitAddMention(id, username) {
|
||||
this.emit(ADD_MENTION_EVENT, id, username);
|
||||
}
|
||||
|
||||
addAddMentionListener(callback) {
|
||||
this.on(ADD_MENTION_EVENT, callback);
|
||||
}
|
||||
|
||||
removeAddMentionListener(callback) {
|
||||
this.removeListener(ADD_MENTION_EVENT, callback);
|
||||
}
|
||||
|
||||
storeSearchResults(results, isMentionSearch) {
|
||||
BrowserStore.setItem('search_results', results);
|
||||
BrowserStore.setItem('is_mention_search', Boolean(isMentionSearch));
|
||||
}
|
||||
}
|
||||
|
||||
var SearchStore = new SearchStoreClass();
|
||||
|
||||
SearchStore.dispatchToken = AppDispatcher.register((payload) => {
|
||||
var action = payload.action;
|
||||
|
||||
switch (action.type) {
|
||||
case ActionTypes.RECIEVED_SEARCH:
|
||||
SearchStore.storeSearchResults(action.results, action.is_mention_search);
|
||||
SearchStore.emitSearchChange();
|
||||
break;
|
||||
case ActionTypes.RECIEVED_SEARCH_TERM:
|
||||
SearchStore.storeSearchTerm(action.term);
|
||||
SearchStore.emitSearchTermChange(action.do_search, action.is_mention_search);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_MENTION_DATA:
|
||||
SearchStore.emitMentionDataChange(action.id, action.mention_text);
|
||||
break;
|
||||
case ActionTypes.RECIEVED_ADD_MENTION:
|
||||
SearchStore.emitAddMention(action.id, action.username);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
export default SearchStore;
|
||||
@@ -86,7 +86,7 @@ class SocketStoreClass extends EventEmitter {
|
||||
|
||||
this.failCount = this.failCount + 1;
|
||||
|
||||
ErrorStore.storeLastError({connErrorCount: this.failCount, message: 'We cannot reach the Mattermost service. The service may be down or misconfigured. Please contact an administrator to make sure the WebSocket port is configured properly.'});
|
||||
ErrorStore.storeLastError({connErrorCount: this.failCount, message: 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.'});
|
||||
ErrorStore.emitChange();
|
||||
};
|
||||
|
||||
@@ -160,7 +160,7 @@ function handleNewPostEvent(msg) {
|
||||
if (window.isActive) {
|
||||
AsyncClient.updateLastViewedAt(true);
|
||||
}
|
||||
} else {
|
||||
} else if (UserStore.getCurrentId() !== msg.user_id || post.type !== Constants.POST_TYPE_JOIN_LEAVE) {
|
||||
AsyncClient.getChannel(msg.channel_id);
|
||||
}
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ export function getChannel(id) {
|
||||
callTracker['getChannel' + id] = utils.getTimestamp();
|
||||
|
||||
client.getChannel(id,
|
||||
function getChannelSuccess(data, textStatus, xhr) {
|
||||
(data, textStatus, xhr) => {
|
||||
callTracker['getChannel' + id] = 0;
|
||||
|
||||
if (xhr.status === 304 || !data) {
|
||||
@@ -145,7 +145,7 @@ export function getChannel(id) {
|
||||
member: data.member
|
||||
});
|
||||
},
|
||||
function getChannelFailure(err) {
|
||||
(err) => {
|
||||
callTracker['getChannel' + id] = 0;
|
||||
dispatchError(err, 'getChannel');
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ function handleError(methodName, xhr, status, err) {
|
||||
|
||||
if (oldError && oldError.connErrorCount) {
|
||||
errorCount += oldError.connErrorCount;
|
||||
connectError = 'We cannot reach the Mattermost service. The service may be down or misconfigured. Please contact an administrator to make sure the WebSocket port is configured properly.';
|
||||
connectError = 'Please check connection, Mattermost unreachable. If issue persists, ask administrator to check WebSocket port.';
|
||||
}
|
||||
|
||||
e = {message: connectError, connErrorCount: errorCount};
|
||||
@@ -328,6 +328,20 @@ export function getConfig(success, error) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getAnalytics(teamId, name, success, error) {
|
||||
$.ajax({
|
||||
url: '/api/v1/admin/analytics/' + teamId + '/' + name,
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
type: 'GET',
|
||||
success,
|
||||
error: (xhr, status, err) => {
|
||||
var e = handleError('getAnalytics', xhr, status, err);
|
||||
error(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function saveConfig(config, success, error) {
|
||||
$.ajax({
|
||||
url: '/api/v1/admin/save_config',
|
||||
|
||||
@@ -98,6 +98,7 @@ module.exports = {
|
||||
POST_LOADING: 'loading',
|
||||
POST_FAILED: 'failed',
|
||||
POST_DELETED: 'deleted',
|
||||
POST_TYPE_JOIN_LEAVE: 'join_leave',
|
||||
RESERVED_TEAM_NAMES: [
|
||||
'www',
|
||||
'web',
|
||||
|
||||
@@ -121,8 +121,11 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
|
||||
paragraph(text) {
|
||||
let outText = text;
|
||||
|
||||
// required so markdown does not strip '_' from @user_names
|
||||
outText = TextFormatting.doFormatMentions(text);
|
||||
|
||||
if (!('emoticons' in this.options) || this.options.emoticon) {
|
||||
outText = TextFormatting.doFormatEmoticons(text);
|
||||
outText = TextFormatting.doFormatEmoticons(outText);
|
||||
}
|
||||
|
||||
if (this.formattingOptions.singleline) {
|
||||
@@ -136,7 +139,7 @@ export class MattermostMarkdownRenderer extends marked.Renderer {
|
||||
return `<table class="markdown__table"><thead>${header}</thead><tbody>${body}</tbody></table>`;
|
||||
}
|
||||
|
||||
text(text) {
|
||||
return TextFormatting.doFormatText(text, this.formattingOptions);
|
||||
text(txt) {
|
||||
return TextFormatting.doFormatText(txt, this.formattingOptions);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +47,8 @@ export function doFormatText(text, options) {
|
||||
const tokens = new Map();
|
||||
|
||||
// replace important words and phrases with tokens
|
||||
output = autolinkUrls(output, tokens);
|
||||
output = autolinkAtMentions(output, tokens);
|
||||
output = autolinkUrls(output, tokens);
|
||||
output = autolinkHashtags(output, tokens);
|
||||
|
||||
if (!('emoticons' in options) || options.emoticon) {
|
||||
@@ -78,6 +78,13 @@ export function doFormatEmoticons(text) {
|
||||
return output;
|
||||
}
|
||||
|
||||
export function doFormatMentions(text) {
|
||||
const tokens = new Map();
|
||||
let output = autolinkAtMentions(text, tokens);
|
||||
output = replaceTokens(output, tokens);
|
||||
return output;
|
||||
}
|
||||
|
||||
export function sanitizeHtml(text) {
|
||||
let output = text;
|
||||
|
||||
@@ -188,6 +195,7 @@ function autolinkAtMentions(text, tokens) {
|
||||
|
||||
let output = text;
|
||||
output = output.replace(/(^|\s)(@([a-z0-9.\-_]*))/gi, replaceAtMentionWithToken);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ var PreferenceStore = require('../stores/preference_store.jsx');
|
||||
var TeamStore = require('../stores/team_store.jsx');
|
||||
var Constants = require('../utils/constants.jsx');
|
||||
var ActionTypes = Constants.ActionTypes;
|
||||
var Client = require('./client.jsx');
|
||||
var AsyncClient = require('./async_client.jsx');
|
||||
var client = require('./client.jsx');
|
||||
var Autolinker = require('autolinker');
|
||||
@@ -1009,3 +1010,44 @@ export function windowWidth() {
|
||||
export function windowHeight() {
|
||||
return $(window).height();
|
||||
}
|
||||
|
||||
export function openDirectChannelToUser(user, successCb, errorCb) {
|
||||
const channelName = this.getDirectChannelName(UserStore.getCurrentId(), user.id);
|
||||
let channel = ChannelStore.getByName(channelName);
|
||||
|
||||
const preference = PreferenceStore.setPreference(Constants.Preferences.CATEGORY_DIRECT_CHANNEL_SHOW, user.id, 'true');
|
||||
AsyncClient.savePreferences([preference]);
|
||||
|
||||
if (channel) {
|
||||
if ($.isFunction(successCb)) {
|
||||
successCb(channel, true);
|
||||
}
|
||||
} else {
|
||||
channel = {
|
||||
name: channelName,
|
||||
last_post_at: 0,
|
||||
total_msg_count: 0,
|
||||
type: 'D',
|
||||
display_name: user.username,
|
||||
teammate_id: user.id,
|
||||
status: UserStore.getStatus(user.id)
|
||||
};
|
||||
|
||||
Client.createDirectChannel(
|
||||
channel,
|
||||
user.id,
|
||||
(data) => {
|
||||
AsyncClient.getChannel(data.id);
|
||||
if ($.isFunction(successCb)) {
|
||||
successCb(data, false);
|
||||
}
|
||||
},
|
||||
() => {
|
||||
window.location.href = TeamStore.getCurrentTeamUrl() + '/channels/' + channelName;
|
||||
if ($.isFunction(errorCb)) {
|
||||
errorCb();
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,63 @@
|
||||
.table {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.total-count {
|
||||
width: 175px;
|
||||
height: 100px;
|
||||
border: 1px solid #ddd;
|
||||
padding: 22px 10px 10px 10px;
|
||||
margin: 10px 10px 10px 10px;
|
||||
background: #fff;
|
||||
float: left;
|
||||
|
||||
> div {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.total-count-by-day {
|
||||
width: 760px;
|
||||
height: 275px;
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px 10px 10px 10px;
|
||||
margin: 10px 10px 10px 10px;
|
||||
background: #fff;
|
||||
clear: both;
|
||||
|
||||
> div {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
}
|
||||
|
||||
.recent-active-users {
|
||||
width: 365px;
|
||||
height: 375px;
|
||||
border: 1px solid #ddd;
|
||||
padding: 5px 10px 10px 10px;
|
||||
margin: 10px 10px 10px 10px;
|
||||
background: #fff;
|
||||
float: left;
|
||||
|
||||
> div {
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
> table {
|
||||
margin: 10px 10px 10px 10px;
|
||||
}
|
||||
|
||||
.recent-active-users-td {
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
border: 1px solid #ddd;
|
||||
padding: 3px 3px 3px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar--left {
|
||||
&.sidebar--collapsable {
|
||||
background: #333;
|
||||
|
||||
@@ -61,3 +61,36 @@
|
||||
@include opacity(1);
|
||||
}
|
||||
}
|
||||
|
||||
#member-list-popover {
|
||||
max-width: initial;
|
||||
.popover-content > div {
|
||||
max-height: 350px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
> div {
|
||||
border-bottom: 1px solid rgba(51,51,51,0.1);
|
||||
padding: 8px 8px 8px 15px;
|
||||
width: 100%;
|
||||
box-sizing: content-box;
|
||||
@include clearfix;
|
||||
.profile-img {
|
||||
border-radius: 50px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.more-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95em;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.more-description {
|
||||
@include opacity(0.7);
|
||||
}
|
||||
.profile-action {
|
||||
margin-left: 8px;
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
11
web/static/js/Chart.min.js
поставляемый
Обычный файл
11
web/static/js/Chart.min.js
поставляемый
Обычный файл
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
@@ -4,6 +4,7 @@
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body>
|
||||
<script src="/static/js/Chart.min.js"></script>
|
||||
|
||||
<div id='error_bar'></div>
|
||||
|
||||
|
||||
14
web/web.go
14
web/web.go
@@ -969,20 +969,20 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
r.ParseForm()
|
||||
|
||||
var props map[string]string
|
||||
var parsedRequest *model.IncomingWebhookRequest
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
props = model.MapFromJson(r.Body)
|
||||
parsedRequest = model.IncomingWebhookRequestFromJson(r.Body)
|
||||
} else {
|
||||
props = model.MapFromJson(strings.NewReader(r.FormValue("payload")))
|
||||
parsedRequest = model.IncomingWebhookRequestFromJson(strings.NewReader(r.FormValue("payload")))
|
||||
}
|
||||
|
||||
text := props["text"]
|
||||
text := parsedRequest.Text
|
||||
if len(text) == 0 {
|
||||
c.Err = model.NewAppError("incomingWebhook", "No text specified", "")
|
||||
return
|
||||
}
|
||||
|
||||
channelName := props["channel"]
|
||||
channelName := parsedRequest.ChannelName
|
||||
|
||||
var hook *model.IncomingWebhook
|
||||
if result := <-hchan; result.Err != nil {
|
||||
@@ -1012,8 +1012,8 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
||||
cchan = api.Srv.Store.Channel().Get(hook.ChannelId)
|
||||
}
|
||||
|
||||
overrideUsername := props["username"]
|
||||
overrideIconUrl := props["icon_url"]
|
||||
overrideUsername := parsedRequest.Username
|
||||
overrideIconUrl := parsedRequest.IconURL
|
||||
|
||||
if result := <-cchan; result.Err != nil {
|
||||
c.Err = model.NewAppError("incomingWebhook", "Couldn't find the channel", "err="+result.Err.Message)
|
||||
|
||||
Ссылка в новой задаче
Block a user