diff --git a/README.md b/README.md index 9fb46572..2fec84fc 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ Each user in Mattermost is connected with their own personal GitLab account. Use ### Sidebar buttons -Team members can stay up-to-date with how many reviews, unread messages, assignments, and open merge requests they have by using buttons in the Mattermost sidebar. +Team members can stay up-to-date with how many reviews, todos, assigned issues, and assigned merge requests they have by using buttons in the Mattermost sidebar. ## Admin guide @@ -142,7 +142,7 @@ Connect your Mattermost account to your GitLab account using `/gitlab connect` a ### Get "To Do" items -Use `/gitlab todo` to get a list of unread messages and merge requests awaiting your review. +Use `/gitlab todo` to get a list of todos, assigned issues, assigned merge requests and merge requests awaiting your review. ### Update settings diff --git a/plugin.json b/plugin.json index f6c9348f..db8fc2da 100644 --- a/plugin.json +++ b/plugin.json @@ -6,7 +6,7 @@ "support_url": "https://github.com/mattermost/mattermost-plugin-gitlab/issues", "release_notes_url": "https://github.com/mattermost/mattermost-plugin-gitlab/releases/tag/v1.8.0", "icon_path": "assets/icon.svg", - "version": "1.8.0", + "version": "1.8.1", "min_server_version": "7.1.0", "server": { "executables": { diff --git a/server/api.go b/server/api.go index 32ff418f..27fd31d4 100644 --- a/server/api.go +++ b/server/api.go @@ -392,10 +392,10 @@ func (p *Plugin) completeConnectUserToGitlab(c *Context, w http.ResponseWriter, "Turn off notifications with `/gitlab settings notifications off`.\n\n"+ "##### Sidebar Buttons\n"+ "Check out the buttons in the left-hand sidebar of Mattermost.\n"+ - "* The first button tells you how many merge requests you have submitted.\n"+ + "* The first button tells you how many merge requests you are assigned to.\n"+ "* The second shows the number of merge requests that are awaiting your review.\n"+ - "* The third shows the number of merge requests and issues you are assigned to.\n"+ - "* The fourth tracks the number of unread messages you have.\n"+ + "* The third shows the number of issues you are assigned to.\n"+ + "* The fourth tracks the number of todos you have.\n"+ "* The fifth will refresh the numbers.\n\n"+ "Click on them!\n\n"+ "##### Slash Commands\n"+ diff --git a/server/command.go b/server/command.go index f115b0d2..9d3f3b53 100644 --- a/server/command.go +++ b/server/command.go @@ -19,7 +19,7 @@ import ( const commandHelp = `* |/gitlab connect| - Connect your Mattermost account to your GitLab account * |/gitlab disconnect| - Disconnect your Mattermost account from your GitLab account -* |/gitlab todo| - Get a list of unread messages and merge requests awaiting your review +* |/gitlab todo| - Get a list of todos, assigned issues, assigned merge requests and merge requests awaiting your review * |/gitlab subscriptions list| - Will list the current channel subscriptions * |/gitlab subscriptions add owner[/repo] [features]| - Subscribe the current channel to receive notifications about opened merge requests and issues for a group or repository * |features| is a comma-delimited list of one or more the following: @@ -248,7 +248,7 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (res _, text, err := p.GetToDo(ctx, info) if err != nil { p.client.Log.Warn("can't get todo in command", "err", err.Error()) - return p.getCommandResponse(args, "Encountered an error getting your to do items."), nil + return p.getCommandResponse(args, "Encountered an error getting your todo items."), nil } return p.getCommandResponse(args, text), nil case "me": @@ -773,7 +773,7 @@ func getAutocompleteData(config *configuration) *model.AutocompleteData { disconnect := model.NewAutocompleteData("disconnect", "", "disconnect your GitLab account") gitlab.AddCommand(disconnect) - todo := model.NewAutocompleteData("todo", "", "Get a list of unread messages and merge requests awaiting your review") + todo := model.NewAutocompleteData("todo", "", "Get a list of todos, assigned issues, assigned merge requests and merge requests awaiting your review") gitlab.AddCommand(todo) subscriptions := model.NewAutocompleteData("subscriptions", "[command]", "Available commands: Add, List, Delete") diff --git a/server/command_test.go b/server/command_test.go index 15a94b55..eeeb2180 100644 --- a/server/command_test.go +++ b/server/command_test.go @@ -380,7 +380,7 @@ func TestAddWebhookCommand(t *testing.T) { p.GitlabClient = mockedClient conf := &model.Config{} - conf.ServiceSettings.SiteURL = &test.siteURL + conf.ServiceSettings.SiteURL = model.NewString(test.siteURL) encryptedToken, _ := encrypt([]byte(testEncryptionKey), testGitlabToken) diff --git a/server/configuration_test.go b/server/configuration_test.go index dd85d1ea..a306716c 100644 --- a/server/configuration_test.go +++ b/server/configuration_test.go @@ -66,7 +66,7 @@ func TestSetDefaults(t *testing.T) { for _, testCase := range []struct { description string isCloud bool - config configuration + config *configuration shouldChange bool outputCheck func(*testing.T, *configuration) @@ -74,7 +74,7 @@ func TestSetDefaults(t *testing.T) { }{ { description: "noop", - config: configuration{ + config: &configuration{ EncryptionKey: "abcd", WebhookSecret: "efgh", }, @@ -85,7 +85,7 @@ func TestSetDefaults(t *testing.T) { }, }, { description: "set encryption key", - config: configuration{ + config: &configuration{ EncryptionKey: "", }, shouldChange: true, @@ -94,7 +94,7 @@ func TestSetDefaults(t *testing.T) { }, }, { description: "set webhook key", - config: configuration{ + config: &configuration{ WebhookSecret: "", }, shouldChange: true, @@ -103,7 +103,7 @@ func TestSetDefaults(t *testing.T) { }, }, { description: "set webhook and encryption key", - config: configuration{ + config: &configuration{ EncryptionKey: "", WebhookSecret: "", }, @@ -115,7 +115,7 @@ func TestSetDefaults(t *testing.T) { }, { description: "Should not set UsePreregisteredApplication in on-prem", isCloud: false, - config: configuration{ + config: &configuration{ EncryptionKey: "abcd", WebhookSecret: "efgh", UsePreregisteredApplication: false, @@ -128,7 +128,7 @@ func TestSetDefaults(t *testing.T) { }, { description: "Should set UsePreregisteredApplication in cloud if no OAuth secret is configured", isCloud: true, - config: configuration{ + config: &configuration{ EncryptionKey: "abcd", WebhookSecret: "efgh", UsePreregisteredApplication: false, @@ -143,7 +143,7 @@ func TestSetDefaults(t *testing.T) { }, { description: "Should set not UsePreregisteredApplication in cloud if OAuth secret is configured", isCloud: true, - config: configuration{ + config: &configuration{ EncryptionKey: "abcd", WebhookSecret: "efgh", UsePreregisteredApplication: false, @@ -163,7 +163,7 @@ func TestSetDefaults(t *testing.T) { changed, err := testCase.config.setDefaults(testCase.isCloud) assert.Equal(t, testCase.shouldChange, changed) - testCase.outputCheck(t, &testCase.config) + testCase.outputCheck(t, testCase.config) if testCase.errMsg != "" { require.Error(t, err) diff --git a/server/flow.go b/server/flow.go index 4f7d77aa..c9a3cfb1 100644 --- a/server/flow.go +++ b/server/flow.go @@ -509,8 +509,8 @@ func (fm *FlowManager) submitOAuthConfig(f *flow.Flow, submitted map[string]inte clientID = strings.TrimSpace(clientID) - if len(clientID) != 64 { - errorList["client_id"] = "Client ID should be 64 characters long" + if len(clientID) < 64 { + errorList["client_id"] = "Client ID should be at least 64 characters long" } clientSecretRaw, ok := submitted["client_secret"] @@ -524,8 +524,8 @@ func (fm *FlowManager) submitOAuthConfig(f *flow.Flow, submitted map[string]inte clientSecret = strings.TrimSpace(clientSecret) - if len(clientSecret) != 64 { - errorList["client_secret"] = "Client Secret should be 64 characters long" + if len(clientSecret) < 64 { + errorList["client_secret"] = "Client Secret should be at least 64 characters long" } if len(errorList) != 0 { diff --git a/server/gitlab/api.go b/server/gitlab/api.go index 0260fb25..951c117d 100644 --- a/server/gitlab/api.go +++ b/server/gitlab/api.go @@ -41,10 +41,10 @@ type Issue struct { } type LHSContent struct { - PRs []*MergeRequest `json:"prs"` - Reviews []*MergeRequest `json:"reviews"` - Assignments []*Issue `json:"assignments"` - Unreads []*internGitlab.Todo `json:"unreads"` + AssignedPRs []*MergeRequest `json:"yourAssignedPrs"` + Reviews []*MergeRequest `json:"reviews"` + AssignedIssues []*Issue `json:"yourAssignedIssues"` + Todos []*internGitlab.Todo `json:"todos"` } // NewGroupHook creates a webhook associated with a GitLab group @@ -303,21 +303,21 @@ func (g *gitlab) GetLHSData(ctx context.Context, user *UserInfo, token *oauth2.T return err }) - var assignments []*Issue + var issues []*Issue grp.Go(func() error { - assignments, err = g.GetYourAssignments(ctx, user, client) + issues, err = g.GetYourAssignedIssues(ctx, user, client) return err }) var mergeRequests []*MergeRequest grp.Go(func() error { - mergeRequests, err = g.GetYourPrs(ctx, user, client) + mergeRequests, err = g.GetYourAssignedPrs(ctx, user, client) return err }) - var unreads []*internGitlab.Todo + var todos []*internGitlab.Todo grp.Go(func() error { - unreads, err = g.GetUnreads(ctx, user, client) + todos, err = g.GetToDoList(ctx, user, client) return err }) @@ -326,10 +326,10 @@ func (g *gitlab) GetLHSData(ctx context.Context, user *UserInfo, token *oauth2.T } return &LHSContent{ - Reviews: reviews, - PRs: mergeRequests, - Assignments: assignments, - Unreads: unreads, + Reviews: reviews, + AssignedPRs: mergeRequests, + AssignedIssues: issues, + Todos: todos, }, nil } @@ -394,13 +394,13 @@ func (g *gitlab) GetReviews(ctx context.Context, user *UserInfo, client *internG return mergeRequests, nil } -func (g *gitlab) GetYourPrs(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*MergeRequest, error) { +func (g *gitlab) GetYourAssignedPrs(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*MergeRequest, error) { opened := stateOpened scope := scopeAll var mrs []*internGitlab.MergeRequest if g.gitlabGroup == "" { opt := &internGitlab.ListMergeRequestsOptions{ - AuthorID: &user.GitlabUserID, + AssigneeID: internGitlab.AssigneeID(user.GitlabUserID), State: &opened, Scope: &scope, ListOptions: internGitlab.ListOptions{Page: 1, PerPage: perPage}, @@ -418,7 +418,7 @@ func (g *gitlab) GetYourPrs(ctx context.Context, user *UserInfo, client *internG } } else { opt := &internGitlab.ListGroupMergeRequestsOptions{ - AuthorID: &user.GitlabUserID, + AssigneeID: internGitlab.AssigneeID(user.GitlabUserID), State: &opened, Scope: &scope, ListOptions: internGitlab.ListOptions{Page: 1, PerPage: perPage}, @@ -547,7 +547,7 @@ func (g *gitlab) fetchYourPrDetails(c context.Context, log logger.Logger, client return nil } -func (g *gitlab) GetYourAssignments(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*Issue, error) { +func (g *gitlab) GetYourAssignedIssues(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*Issue, error) { opened := stateOpened scope := scopeAll var issues []*internGitlab.Issue @@ -607,7 +607,7 @@ func (g *gitlab) GetYourAssignments(ctx context.Context, user *UserInfo, client return result, nil } -func (g *gitlab) GetUnreads(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*internGitlab.Todo, error) { +func (g *gitlab) GetToDoList(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*internGitlab.Todo, error) { var todos []*internGitlab.Todo opt := &internGitlab.ListTodosOptions{ diff --git a/server/gitlab/gitlab.go b/server/gitlab/gitlab.go index 20bbb0e5..eede91d1 100644 --- a/server/gitlab/gitlab.go +++ b/server/gitlab/gitlab.go @@ -29,10 +29,10 @@ type Gitlab interface { GetProject(ctx context.Context, user *UserInfo, token *oauth2.Token, owner, repo string) (*internGitlab.Project, error) GetYourPrDetails(ctx context.Context, log logger.Logger, user *UserInfo, token *oauth2.Token, prList []*PRDetails) ([]*PRDetails, error) GetReviews(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*MergeRequest, error) - GetYourPrs(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*MergeRequest, error) + GetYourAssignedPrs(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*MergeRequest, error) GetLHSData(ctx context.Context, user *UserInfo, token *oauth2.Token) (*LHSContent, error) - GetYourAssignments(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*Issue, error) - GetUnreads(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*internGitlab.Todo, error) + GetYourAssignedIssues(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*Issue, error) + GetToDoList(ctx context.Context, user *UserInfo, client *internGitlab.Client) ([]*internGitlab.Todo, error) GetProjectHooks(ctx context.Context, user *UserInfo, token *oauth2.Token, owner string, repo string) ([]*WebhookInfo, error) GetGroupHooks(ctx context.Context, user *UserInfo, token *oauth2.Token, owner string) ([]*WebhookInfo, error) NewProjectHook(ctx context.Context, user *UserInfo, token *oauth2.Token, projectID interface{}, projectHookOptions *AddWebhookOptions) (*WebhookInfo, error) diff --git a/server/gitlab/mocks/mock_gitlab.go b/server/gitlab/mocks/mock_gitlab.go index 668bed95..d75c91e2 100644 --- a/server/gitlab/mocks/mock_gitlab.go +++ b/server/gitlab/mocks/mock_gitlab.go @@ -128,19 +128,19 @@ func (mr *MockGitlabMockRecorder) GetReviews(arg0, arg1, arg2 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReviews", reflect.TypeOf((*MockGitlab)(nil).GetReviews), arg0, arg1, arg2) } -// GetUnreads mocks base method. -func (m *MockGitlab) GetUnreads(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab0.Todo, error) { +// GetToDoList mocks base method. +func (m *MockGitlab) GetToDoList(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab0.Todo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUnreads", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetToDoList", arg0, arg1, arg2) ret0, _ := ret[0].([]*gitlab0.Todo) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetUnreads indicates an expected call of GetUnreads. -func (mr *MockGitlabMockRecorder) GetUnreads(arg0, arg1, arg2 interface{}) *gomock.Call { +// GetToDoList indicates an expected call of GetToDoList. +func (mr *MockGitlabMockRecorder) GetToDoList(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnreads", reflect.TypeOf((*MockGitlab)(nil).GetUnreads), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToDoList", reflect.TypeOf((*MockGitlab)(nil).GetToDoList), arg0, arg1, arg2) } // GetUserDetails mocks base method. @@ -158,49 +158,49 @@ func (mr *MockGitlabMockRecorder) GetUserDetails(arg0, arg1, arg2 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserDetails", reflect.TypeOf((*MockGitlab)(nil).GetUserDetails), arg0, arg1, arg2) } -// GetYourAssignments mocks base method. -func (m *MockGitlab) GetYourAssignments(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab.Issue, error) { +// GetYourAssignedIssues mocks base method. +func (m *MockGitlab) GetYourAssignedIssues(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab.Issue, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetYourAssignments", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetYourAssignedIssues", arg0, arg1, arg2) ret0, _ := ret[0].([]*gitlab.Issue) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetYourAssignments indicates an expected call of GetYourAssignments. -func (mr *MockGitlabMockRecorder) GetYourAssignments(arg0, arg1, arg2 interface{}) *gomock.Call { +// GetYourAssignedIssues indicates an expected call of GetYourAssignedIssues. +func (mr *MockGitlabMockRecorder) GetYourAssignedIssues(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourAssignments", reflect.TypeOf((*MockGitlab)(nil).GetYourAssignments), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourAssignedIssues", reflect.TypeOf((*MockGitlab)(nil).GetYourAssignedIssues), arg0, arg1, arg2) } -// GetYourPrDetails mocks base method. -func (m *MockGitlab) GetYourPrDetails(arg0 context.Context, arg1 logger.Logger, arg2 *gitlab.UserInfo, arg3 *oauth2.Token, arg4 []*gitlab.PRDetails) ([]*gitlab.PRDetails, error) { +// GetYourAssignedPrs mocks base method. +func (m *MockGitlab) GetYourAssignedPrs(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab.MergeRequest, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetYourPrDetails", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].([]*gitlab.PRDetails) + ret := m.ctrl.Call(m, "GetYourAssignedPrs", arg0, arg1, arg2) + ret0, _ := ret[0].([]*gitlab.MergeRequest) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetYourPrDetails indicates an expected call of GetYourPrDetails. -func (mr *MockGitlabMockRecorder) GetYourPrDetails(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { +// GetYourAssignedPrs indicates an expected call of GetYourAssignedPrs. +func (mr *MockGitlabMockRecorder) GetYourAssignedPrs(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourPrDetails", reflect.TypeOf((*MockGitlab)(nil).GetYourPrDetails), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourAssignedPrs", reflect.TypeOf((*MockGitlab)(nil).GetYourAssignedPrs), arg0, arg1, arg2) } -// GetYourPrs mocks base method. -func (m *MockGitlab) GetYourPrs(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab.MergeRequest, error) { +// GetYourPrDetails mocks base method. +func (m *MockGitlab) GetYourPrDetails(arg0 context.Context, arg1 logger.Logger, arg2 *gitlab.UserInfo, arg3 *oauth2.Token, arg4 []*gitlab.PRDetails) ([]*gitlab.PRDetails, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetYourPrs", arg0, arg1, arg2) - ret0, _ := ret[0].([]*gitlab.MergeRequest) + ret := m.ctrl.Call(m, "GetYourPrDetails", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].([]*gitlab.PRDetails) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetYourPrs indicates an expected call of GetYourPrs. -func (mr *MockGitlabMockRecorder) GetYourPrs(arg0, arg1, arg2 interface{}) *gomock.Call { +// GetYourPrDetails indicates an expected call of GetYourPrDetails. +func (mr *MockGitlabMockRecorder) GetYourPrDetails(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourPrs", reflect.TypeOf((*MockGitlab)(nil).GetYourPrs), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourPrDetails", reflect.TypeOf((*MockGitlab)(nil).GetYourPrDetails), arg0, arg1, arg2, arg3, arg4) } // GitlabConnect mocks base method. diff --git a/server/mocks/mock_gitlab.go b/server/mocks/mock_gitlab.go index 668bed95..d75c91e2 100644 --- a/server/mocks/mock_gitlab.go +++ b/server/mocks/mock_gitlab.go @@ -128,19 +128,19 @@ func (mr *MockGitlabMockRecorder) GetReviews(arg0, arg1, arg2 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetReviews", reflect.TypeOf((*MockGitlab)(nil).GetReviews), arg0, arg1, arg2) } -// GetUnreads mocks base method. -func (m *MockGitlab) GetUnreads(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab0.Todo, error) { +// GetToDoList mocks base method. +func (m *MockGitlab) GetToDoList(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab0.Todo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetUnreads", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetToDoList", arg0, arg1, arg2) ret0, _ := ret[0].([]*gitlab0.Todo) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetUnreads indicates an expected call of GetUnreads. -func (mr *MockGitlabMockRecorder) GetUnreads(arg0, arg1, arg2 interface{}) *gomock.Call { +// GetToDoList indicates an expected call of GetToDoList. +func (mr *MockGitlabMockRecorder) GetToDoList(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUnreads", reflect.TypeOf((*MockGitlab)(nil).GetUnreads), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetToDoList", reflect.TypeOf((*MockGitlab)(nil).GetToDoList), arg0, arg1, arg2) } // GetUserDetails mocks base method. @@ -158,49 +158,49 @@ func (mr *MockGitlabMockRecorder) GetUserDetails(arg0, arg1, arg2 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserDetails", reflect.TypeOf((*MockGitlab)(nil).GetUserDetails), arg0, arg1, arg2) } -// GetYourAssignments mocks base method. -func (m *MockGitlab) GetYourAssignments(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab.Issue, error) { +// GetYourAssignedIssues mocks base method. +func (m *MockGitlab) GetYourAssignedIssues(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab.Issue, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetYourAssignments", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "GetYourAssignedIssues", arg0, arg1, arg2) ret0, _ := ret[0].([]*gitlab.Issue) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetYourAssignments indicates an expected call of GetYourAssignments. -func (mr *MockGitlabMockRecorder) GetYourAssignments(arg0, arg1, arg2 interface{}) *gomock.Call { +// GetYourAssignedIssues indicates an expected call of GetYourAssignedIssues. +func (mr *MockGitlabMockRecorder) GetYourAssignedIssues(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourAssignments", reflect.TypeOf((*MockGitlab)(nil).GetYourAssignments), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourAssignedIssues", reflect.TypeOf((*MockGitlab)(nil).GetYourAssignedIssues), arg0, arg1, arg2) } -// GetYourPrDetails mocks base method. -func (m *MockGitlab) GetYourPrDetails(arg0 context.Context, arg1 logger.Logger, arg2 *gitlab.UserInfo, arg3 *oauth2.Token, arg4 []*gitlab.PRDetails) ([]*gitlab.PRDetails, error) { +// GetYourAssignedPrs mocks base method. +func (m *MockGitlab) GetYourAssignedPrs(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab.MergeRequest, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetYourPrDetails", arg0, arg1, arg2, arg3, arg4) - ret0, _ := ret[0].([]*gitlab.PRDetails) + ret := m.ctrl.Call(m, "GetYourAssignedPrs", arg0, arg1, arg2) + ret0, _ := ret[0].([]*gitlab.MergeRequest) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetYourPrDetails indicates an expected call of GetYourPrDetails. -func (mr *MockGitlabMockRecorder) GetYourPrDetails(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { +// GetYourAssignedPrs indicates an expected call of GetYourAssignedPrs. +func (mr *MockGitlabMockRecorder) GetYourAssignedPrs(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourPrDetails", reflect.TypeOf((*MockGitlab)(nil).GetYourPrDetails), arg0, arg1, arg2, arg3, arg4) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourAssignedPrs", reflect.TypeOf((*MockGitlab)(nil).GetYourAssignedPrs), arg0, arg1, arg2) } -// GetYourPrs mocks base method. -func (m *MockGitlab) GetYourPrs(arg0 context.Context, arg1 *gitlab.UserInfo, arg2 *gitlab0.Client) ([]*gitlab.MergeRequest, error) { +// GetYourPrDetails mocks base method. +func (m *MockGitlab) GetYourPrDetails(arg0 context.Context, arg1 logger.Logger, arg2 *gitlab.UserInfo, arg3 *oauth2.Token, arg4 []*gitlab.PRDetails) ([]*gitlab.PRDetails, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetYourPrs", arg0, arg1, arg2) - ret0, _ := ret[0].([]*gitlab.MergeRequest) + ret := m.ctrl.Call(m, "GetYourPrDetails", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].([]*gitlab.PRDetails) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetYourPrs indicates an expected call of GetYourPrs. -func (mr *MockGitlabMockRecorder) GetYourPrs(arg0, arg1, arg2 interface{}) *gomock.Call { +// GetYourPrDetails indicates an expected call of GetYourPrDetails. +func (mr *MockGitlabMockRecorder) GetYourPrDetails(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourPrs", reflect.TypeOf((*MockGitlab)(nil).GetYourPrs), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetYourPrDetails", reflect.TypeOf((*MockGitlab)(nil).GetYourPrDetails), arg0, arg1, arg2, arg3, arg4) } // GitlabConnect mocks base method. diff --git a/server/plugin.go b/server/plugin.go index b980f3dd..52d93318 100644 --- a/server/plugin.go +++ b/server/plugin.go @@ -586,11 +586,11 @@ func (p *Plugin) GetToDo(ctx context.Context, user *gitlab.UserInfo) (bool, stri return err } - unreads := resp.Unreads + todos := resp.Todos notificationCount := 0 notificationContent := "" - for _, n := range unreads { + for _, n := range todos { if n == nil { continue } @@ -610,9 +610,9 @@ func (p *Plugin) GetToDo(ctx context.Context, user *gitlab.UserInfo) (bool, stri } if notificationCount == 0 { - notificationText += "You don't have any unread messages.\n" + notificationText += "You don't have any todos.\n" } else { - notificationText += fmt.Sprintf("You have %v unread messages:\n", notificationCount) + notificationText += fmt.Sprintf("You have %v todos:\n", notificationCount) notificationText += notificationContent hasTodo = true @@ -631,25 +631,24 @@ func (p *Plugin) GetToDo(ctx context.Context, user *gitlab.UserInfo) (bool, stri hasTodo = true } - yourAssignments := resp.Assignments - - if len(yourAssignments) == 0 { + yourAssignedIssues := resp.AssignedIssues + if len(yourAssignedIssues) == 0 { assignmentText += "You don't have any issues awaiting your dev.\n" } else { - assignmentText += fmt.Sprintf("You have %v issues awaiting dev:\n", len(yourAssignments)) + assignmentText += fmt.Sprintf("You have %v issues awaiting dev:\n", len(yourAssignedIssues)) - for _, pr := range yourAssignments { + for _, pr := range yourAssignedIssues { assignmentText += fmt.Sprintf("* [%v](%v)\n", pr.Title, pr.WebURL) } hasTodo = true } - mergeRequests := resp.PRs + mergeRequests := resp.AssignedPRs if len(mergeRequests) == 0 { - mergeRequestText += "You don't have any open merge requests.\n" + mergeRequestText += "You don't have any merge requests assigned.\n" } else { - mergeRequestText += fmt.Sprintf("You have %v open merge requests:\n", len(mergeRequests)) + mergeRequestText += fmt.Sprintf("You have %v merge requests assigned:\n", len(mergeRequests)) for _, pr := range mergeRequests { mergeRequestText += fmt.Sprintf("* [%v](%v)\n", pr.Title, pr.WebURL) @@ -664,16 +663,16 @@ func (p *Plugin) GetToDo(ctx context.Context, user *gitlab.UserInfo) (bool, stri return false, "", err } - text := "##### Unread Messages\n" + text := "##### To-Do list\n" text += notificationText text += "##### Review Requests\n" text += reviewText - text += "##### Assignments\n" + text += "##### Issues\n" text += assignmentText - text += "##### Your Open Merge Requests\n" + text += "##### Merge Requests Assigned\n" text += mergeRequestText return hasTodo, text, nil diff --git a/server/webhook/merge_request.go b/server/webhook/merge_request.go index 11b50fe9..cfc82399 100644 --- a/server/webhook/merge_request.go +++ b/server/webhook/merge_request.go @@ -23,6 +23,11 @@ func (w *webhook) handleDMMergeRequest(event *gitlab.MergeEvent) ([]*HandleWebho authorGitlabUsername := w.gitlabRetreiver.GetUsernameByID(event.ObjectAttributes.AuthorID) senderGitlabUsername := event.User.Username + toUsers := []string{authorGitlabUsername} + for _, assigneeID := range event.ObjectAttributes.AssigneeIDs { + toUsers = append(toUsers, w.gitlabRetreiver.GetUsernameByID(assigneeID)) + } + message := "" switch event.ObjectAttributes.State { @@ -31,9 +36,28 @@ func (w *webhook) handleDMMergeRequest(event *gitlab.MergeEvent) ([]*HandleWebho case actionOpen: message = fmt.Sprintf("[%s](%s) requested your review on [%s!%v](%s)", senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername), event.ObjectAttributes.Target.PathWithNamespace, event.ObjectAttributes.IID, event.ObjectAttributes.URL) case actionReopen: - message = fmt.Sprintf("[%s](%s) reopen your merge request [%s!%v](%s)", senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername), event.ObjectAttributes.Target.PathWithNamespace, event.ObjectAttributes.IID, event.ObjectAttributes.URL) + message = fmt.Sprintf("[%s](%s) reopened your merge request [%s!%v](%s)", senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername), event.ObjectAttributes.Target.PathWithNamespace, event.ObjectAttributes.IID, event.ObjectAttributes.URL) case actionUpdate: - // TODO not enough check (opened/update) to say assigned to you... + toUsers = []string{authorGitlabUsername} + + // Not going to show notification in case of commit push. + if event.ObjectAttributes.OldRev != "" { + break + } + + for _, currentAssigneeID := range event.ObjectAttributes.AssigneeIDs { + assignedInPrevious := false + for _, previousAssignee := range event.Changes.Assignees.Previous { + if previousAssignee.ID == currentAssigneeID { + assignedInPrevious = true + break + } + } + if !assignedInPrevious { + toUsers = append(toUsers, w.gitlabRetreiver.GetUsernameByID(currentAssigneeID)) + } + } + message = fmt.Sprintf("[%s](%s) assigned you to merge request [%s!%v](%s)", senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername), event.ObjectAttributes.Target.PathWithNamespace, event.ObjectAttributes.IID, event.ObjectAttributes.URL) case actionApproved: message = fmt.Sprintf("[%s](%s) approved your merge request [%s!%v](%s)", senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername), event.ObjectAttributes.Target.PathWithNamespace, event.ObjectAttributes.IID, event.ObjectAttributes.URL) @@ -51,7 +75,7 @@ func (w *webhook) handleDMMergeRequest(event *gitlab.MergeEvent) ([]*HandleWebho if len(message) > 0 { handlers := []*HandleWebhook{{ Message: message, - ToUsers: []string{w.gitlabRetreiver.GetUsernameByID(event.ObjectAttributes.AssigneeID), authorGitlabUsername}, + ToUsers: toUsers, ToChannels: []string{}, From: senderGitlabUsername, }} diff --git a/server/webhook/merge_request_fixture_test.go b/server/webhook/merge_request_fixture_test.go index fcdd71e9..a92451cc 100644 --- a/server/webhook/merge_request_fixture_test.go +++ b/server/webhook/merge_request_fixture_test.go @@ -105,6 +105,9 @@ const OpenMergeRequest = `{ "total_time_spent":0, "human_total_time_spent":null, "human_time_estimate":null, + "assignee_ids": [ + 50 + ], "action":"open" }, "labels":[], @@ -650,6 +653,9 @@ const AssigneeMergeRequest = `{ "total_time_spent":0, "human_total_time_spent":null, "human_time_estimate":null, + "assignee_ids": [ + 50 + ], "action":"update" }, "labels":[], diff --git a/server/webhook/merge_request_test.go b/server/webhook/merge_request_test.go index e557dbf8..fa5dcbd3 100644 --- a/server/webhook/merge_request_test.go +++ b/server/webhook/merge_request_test.go @@ -72,13 +72,13 @@ var testDataMergeRequest = []testDataMergeRequestStr{ From: "manland", }}, }, { - testTitle: "manland reopen merge request of root and display in channel1", + testTitle: "manland reopened merge request of root and display in channel1", fixture: ReopenMerge, gitlabRetreiver: newFakeWebhook([]*subscription.Subscription{ {ChannelID: "channel1", CreatorID: "1", Features: "merges", Repository: "manland/webhook"}, }), res: []*HandleWebhook{{ - Message: "[manland](http://my.gitlab.com/manland) reopen your merge request [manland/webhook!1](http://localhost:3000/manland/webhook/merge_requests/1)", + Message: "[manland](http://my.gitlab.com/manland) reopened your merge request [manland/webhook!1](http://localhost:3000/manland/webhook/merge_requests/1)", ToUsers: []string{"root"}, ToChannels: []string{}, From: "manland", diff --git a/server/webhook/note.go b/server/webhook/note.go index d188bd62..4865bc9e 100644 --- a/server/webhook/note.go +++ b/server/webhook/note.go @@ -42,7 +42,7 @@ func (w *webhook) handleDMIssueComment(event *gitlab.IssueCommentEvent) ([]*Hand pathWithNamespace: event.Project.PathWithNamespace, IID: fmt.Sprintf("%d", event.Issue.IID), URL: event.ObjectAttributes.URL, - body: event.ObjectAttributes.Note, + body: event.ObjectAttributes.Description, }); mention != nil { handlers = append(handlers, mention) } @@ -53,7 +53,7 @@ func (w *webhook) handleDMIssueComment(event *gitlab.IssueCommentEvent) ([]*Hand func (w *webhook) handleChannelIssueComment(ctx context.Context, event *gitlab.IssueCommentEvent) ([]*HandleWebhook, error) { senderGitlabUsername := event.User.Username repo := event.Project - body := event.ObjectAttributes.Note + body := event.ObjectAttributes.Description res := []*HandleWebhook{} message := fmt.Sprintf("[%s](%s) New comment by [%s](%s) on [#%v %s](%s):\n\n%s", repo.PathWithNamespace, repo.WebURL, senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername), event.Issue.IID, event.Issue.Title, event.ObjectAttributes.URL, body) @@ -113,7 +113,7 @@ func (w *webhook) handleDMMergeRequestComment(event *gitlab.MergeCommentEvent) ( pathWithNamespace: event.Project.PathWithNamespace, IID: fmt.Sprintf("%d", event.MergeRequest.IID), URL: event.ObjectAttributes.URL, - body: event.ObjectAttributes.Note, + body: event.ObjectAttributes.Description, }); mention != nil { handlers = append(handlers, mention) } @@ -123,7 +123,7 @@ func (w *webhook) handleDMMergeRequestComment(event *gitlab.MergeCommentEvent) ( func (w *webhook) handleChannelMergeRequestComment(ctx context.Context, event *gitlab.MergeCommentEvent) ([]*HandleWebhook, error) { senderGitlabUsername := event.User.Username repo := event.Project - body := event.ObjectAttributes.Note + body := event.ObjectAttributes.Description res := []*HandleWebhook{} message := fmt.Sprintf("[%s](%s) New comment by [%s](%s) on [#%v %s](%s):\n\n%s", repo.PathWithNamespace, repo.WebURL, senderGitlabUsername, w.gitlabRetreiver.GetUserURL(senderGitlabUsername), event.MergeRequest.IID, event.MergeRequest.Title, event.ObjectAttributes.URL, body) diff --git a/webapp/src/actions/index.js b/webapp/src/actions/index.js index 1b99348f..95f20fe4 100644 --- a/webapp/src/actions/index.js +++ b/webapp/src/actions/index.js @@ -1,3 +1,7 @@ +import {getCurrentChannelId, getCurrentUserId} from 'mattermost-redux/selectors/entities/common'; + +import {PostTypes} from 'mattermost-redux/action_types'; + import Client from '../client'; import ActionTypes from '../action_types'; import {id} from '../manifest'; @@ -223,3 +227,28 @@ export function getChannelSubscriptions(channelId) { return {subscriptions}; }; } + +export function sendEphemeralPost(message) { + return (dispatch, getState) => { + const timestamp = Date.now(); + const state = getState(); + + const post = { + id: 'gitlabPlugin' + Date.now(), + user_id: getCurrentUserId(state), + channel_id: getCurrentChannelId(state), + message, + type: 'system_ephemeral', + create_at: timestamp, + update_at: timestamp, + root_id: '', + parent_id: '', + props: {}, + }; + + dispatch({ + type: PostTypes.RECEIVED_NEW_POST, + data: post, + }); + }; +} diff --git a/webapp/src/components/rhs_sidebar/index.js b/webapp/src/components/rhs_sidebar/index.js index 0bf219c5..2dc45b81 100644 --- a/webapp/src/components/rhs_sidebar/index.js +++ b/webapp/src/components/rhs_sidebar/index.js @@ -7,6 +7,7 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users'; import {id} from '../../manifest'; import { getChannelSubscriptions, + sendEphemeralPost, } from '../../actions'; import {getPluginServerRoute} from '../../selectors'; @@ -34,6 +35,7 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators( { getChannelSubscriptions, + sendEphemeralPost, }, dispatch, ), diff --git a/webapp/src/components/rhs_sidebar/rhs_sidebar.jsx b/webapp/src/components/rhs_sidebar/rhs_sidebar.jsx index 1e560984..44166f35 100644 --- a/webapp/src/components/rhs_sidebar/rhs_sidebar.jsx +++ b/webapp/src/components/rhs_sidebar/rhs_sidebar.jsx @@ -1,6 +1,9 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {isDesktopApp} from 'src/utils/user_agent'; +import {connectUsingBrowserMessage} from 'src/constants'; + import MattermostGitLabSVG from './mattermost_gitlab'; import NoSubscriptionsSVG from './no_subscriptions'; @@ -9,6 +12,10 @@ import './rhs_sidebar.css'; const NotSignedIn = (props) => { const openConnectWindow = (e) => { e.preventDefault(); + if (isDesktopApp()) { + props.sendEphemeralPost(connectUsingBrowserMessage); + return; + } window.open(`${props.pluginServerRoute}/oauth/connect`, 'Connect Mattermost to GitLab', 'height=570,width=520'); }; @@ -33,6 +40,7 @@ const NotSignedIn = (props) => { NotSignedIn.propTypes = { pluginServerRoute: PropTypes.string.isRequired, + sendEphemeralPost: PropTypes.func.isRequired, }; const UserHeader = (props) => ( @@ -140,6 +148,7 @@ export default class RHSSidebar extends React.PureComponent { pluginServerRoute: PropTypes.string.isRequired, actions: PropTypes.shape({ getChannelSubscriptions: PropTypes.func.isRequired, + sendEphemeralPost: PropTypes.func.isRequired, }).isRequired, }; @@ -182,6 +191,7 @@ export default class RHSSidebar extends React.PureComponent { return ( ); } diff --git a/webapp/src/components/sidebar_buttons/button_icons.tsx b/webapp/src/components/sidebar_buttons/button_icons.tsx new file mode 100644 index 00000000..518bb71d --- /dev/null +++ b/webapp/src/components/sidebar_buttons/button_icons.tsx @@ -0,0 +1,17 @@ +import React from 'react'; + +export const GitLabIssuesIcon = ({fill}: {fill: string}) => ( + +) + +export const GitLabMergeRequestIcon = ({fill}: {fill: string}) => ( + +) + +export const GitLabReviewsIcon = ({fill}: {fill: string}) => ( + +) + +export const GitLabTodosIcon = ({fill}: {fill: string}) => ( + +) diff --git a/webapp/src/components/sidebar_buttons/index.js b/webapp/src/components/sidebar_buttons/index.js index 4ad03720..45ef74c3 100644 --- a/webapp/src/components/sidebar_buttons/index.js +++ b/webapp/src/components/sidebar_buttons/index.js @@ -3,6 +3,7 @@ import {bindActionCreators} from 'redux'; import { updateRHSState, + sendEphemeralPost, getLHSData, } from '../../actions'; @@ -18,9 +19,9 @@ function mapStateToProps(state) { username: state[`plugins-${id}`].username, clientId: state[`plugins-${id}`].clientId, reviews: state[`plugins-${id}`].lhsData?.reviews, - yourPrs: state[`plugins-${id}`].lhsData?.prs, - yourAssignments: state[`plugins-${id}`].lhsData?.assignments, - unreads: state[`plugins-${id}`].lhsData?.unreads, + yourAssignedPrs: state[`plugins-${id}`].lhsData?.yourAssignedPrs, + yourAssignedIssues: state[`plugins-${id}`].lhsData?.yourAssignedIssues, + todos: state[`plugins-${id}`].lhsData?.todos, gitlabURL: state[`plugins-${id}`].gitlabURL, org: state[`plugins-${id}`].organization, pluginServerRoute: getPluginServerRoute(state), @@ -33,6 +34,7 @@ function mapDispatchToProps(dispatch) { actions: bindActionCreators( { updateRHSState, + sendEphemeralPost, getLHSData, }, dispatch, diff --git a/webapp/src/components/sidebar_buttons/sidebar_buttons.jsx b/webapp/src/components/sidebar_buttons/sidebar_buttons.jsx index 3b4c1f17..1fdad391 100644 --- a/webapp/src/components/sidebar_buttons/sidebar_buttons.jsx +++ b/webapp/src/components/sidebar_buttons/sidebar_buttons.jsx @@ -3,7 +3,10 @@ import {Tooltip, OverlayTrigger} from 'react-bootstrap'; import PropTypes from 'prop-types'; import {makeStyleFromTheme, changeOpacity} from 'mattermost-redux/utils/theme_utils'; -import {RHSStates} from 'src/constants'; +import {RHSStates, connectUsingBrowserMessage} from 'src/constants'; +import {isDesktopApp} from 'src/utils/user_agent'; + +import {GitLabIssuesIcon, GitLabMergeRequestIcon, GitLabReviewsIcon, GitLabTodosIcon} from './button_icons'; export default class SidebarButtons extends React.PureComponent { static propTypes = { @@ -14,14 +17,15 @@ export default class SidebarButtons extends React.PureComponent { clientId: PropTypes.string, gitlabURL: PropTypes.string, reviews: PropTypes.arrayOf(PropTypes.object), - unreads: PropTypes.arrayOf(PropTypes.object), - yourPrs: PropTypes.arrayOf(PropTypes.object), - yourAssignments: PropTypes.arrayOf(PropTypes.object), + todos: PropTypes.arrayOf(PropTypes.object), + yourAssignedPrs: PropTypes.arrayOf(PropTypes.object), + yourAssignedIssues: PropTypes.arrayOf(PropTypes.object), isTeamSidebar: PropTypes.bool, pluginServerRoute: PropTypes.string.isRequired, showRHSPlugin: PropTypes.func.isRequired, actions: PropTypes.shape({ updateRHSState: PropTypes.func.isRequired, + sendEphemeralPost: PropTypes.func.isRequired, getLHSData: PropTypes.func.isRequired, }).isRequired, }; @@ -62,6 +66,10 @@ export default class SidebarButtons extends React.PureComponent { openConnectWindow = (e) => { e.preventDefault(); + if (isDesktopApp()) { + this.props.actions.sendEphemeralPost(connectUsingBrowserMessage); + return; + } window.open(`${this.props.pluginServerRoute}/oauth/connect`, 'Connect Mattermost to GitLab', 'height=570,width=520'); }; @@ -106,9 +114,9 @@ export default class SidebarButtons extends React.PureComponent { const baseURL = this.props.gitlabURL || 'https://gitlab.com'; const reviews = this.props.reviews || []; - const yourPrs = this.props.yourPrs || []; - const unreads = this.props.unreads || []; - const yourAssignments = this.props.yourAssignments || []; + const yourAssignedPrs = this.props.yourAssignedPrs || []; + const todos = this.props.todos || []; + const yourAssignedIssues = this.props.yourAssignedIssues || []; const refreshClass = this.state.refreshing ? ' fa-spin' : ''; return ( @@ -123,55 +131,55 @@ export default class SidebarButtons extends React.PureComponent { {'Your open merge requests'}} + overlay={{'Merge requests assigned'}} > this.openRHS(RHSStates.PRS)} style={button} > - - {' ' + yourPrs.length} + + {yourAssignedPrs.length} {'Merge requests that need review'}} + overlay={{'Merge requests needing review'}} > this.openRHS(RHSStates.REVIEWS)} style={button} > - - {' ' + reviews.length} + + {reviews.length} {'Your assignments'}} + overlay={{'Issues'}} > this.openRHS(RHSStates.ASSIGNMENTS)} + onClick={() => this.openRHS(RHSStates.ISSUES)} style={button} > - - {' ' + yourAssignments.length} + + {yourAssignedIssues.length} {'Unread messages'}} + overlay={{'To-Do list'}} > this.openRHS(RHSStates.UNREADS)} + onClick={() => this.openRHS(RHSStates.TODOS)} style={button} > - - {' ' + unreads.length} + + {todos.length} { color: changeOpacity(theme.sidebarText, 0.6), textAlign: 'center', cursor: 'pointer', + display: 'flex', + alignItems: 'center', + }, + buttonCount: { + marginLeft: '2px', }, containerHeader: { marginTop: '10px', diff --git a/webapp/src/components/sidebar_right/index.tsx b/webapp/src/components/sidebar_right/index.tsx index b161f137..a5f23942 100644 --- a/webapp/src/components/sidebar_right/index.tsx +++ b/webapp/src/components/sidebar_right/index.tsx @@ -24,9 +24,9 @@ interface Props { org: string; gitlabURL: string; reviews: Item[]; - unreads: Item[], - yourPrs: Item[], - yourAssignments: Item[], + todos: Item[], + yourAssignedPrs: Item[], + yourAssignedIssues: Item[], rhsState: string, theme: Theme, } @@ -72,18 +72,18 @@ function shouldUpdateDetails(prs: Item[], prevPrs: Item[], targetState: string, function SidebarRight({theme}: {theme: Theme}) { const sidebarData = useSelector(getSidebarData); - const {username, yourAssignments, org, unreads, gitlabURL, rhsState, reviews, yourPrs} = sidebarData; + const {username, yourAssignedIssues, org, todos, gitlabURL, rhsState, reviews, yourAssignedPrs} = sidebarData; const dispatch = useDispatch(); - const prevPrs = usePrevious(yourPrs) + const prevPrs = usePrevious(yourAssignedPrs) const prevReviews = usePrevious(reviews) useEffect(() => { - if (yourPrs && (!prevPrs || shouldUpdateDetails(yourPrs, prevPrs, RHSStates.PRS, rhsState))) { - dispatch(getYourPrDetails(yourPrs)); + if (yourAssignedPrs && (!prevPrs || shouldUpdateDetails(yourAssignedPrs, prevPrs, RHSStates.PRS, rhsState))) { + dispatch(getYourPrDetails(yourAssignedPrs)); } - }, [yourPrs, rhsState, prevPrs]); + }, [yourAssignedPrs, rhsState, prevPrs]); useEffect(() => { if (reviews && (!prevReviews || shouldUpdateDetails(reviews, prevReviews, RHSStates.REVIEWS, rhsState))) { @@ -104,23 +104,23 @@ function SidebarRight({theme}: {theme: Theme}) { switch (rhsState) { case RHSStates.PRS: - gitlabItems = yourPrs; - title = 'Your Open Merge Requests'; - listUrl = `${baseURL}${orgQuery}/merge_requests?state=opened&author_username=${username}`; + gitlabItems = yourAssignedPrs; + title = 'Merge Requests Assigned'; + listUrl = `${baseURL}${orgQuery}/merge_requests?state=opened&assignee_username=${username}`; break; case RHSStates.REVIEWS: gitlabItems = reviews; listUrl = `${baseURL}${orgQuery}/merge_requests?reviewer_username=${username}`; title = 'Merge Requests Needing Review'; break; - case RHSStates.UNREADS: - gitlabItems = unreads; - title = 'Unread Messages'; + case RHSStates.TODOS: + gitlabItems = todos; + title = 'To-Do List'; listUrl = `${baseURL}/dashboard/todos`; break; - case RHSStates.ASSIGNMENTS: - gitlabItems = yourAssignments; - title = 'Your Assignments'; + case RHSStates.ISSUES: + gitlabItems = yourAssignedIssues; + title = 'Issues'; listUrl = `${baseURL}${orgQuery}/issues?assignee_username=${username}`; break; default: diff --git a/webapp/src/constants/index.js b/webapp/src/constants/index.js index 2c1da24b..eb02975f 100644 --- a/webapp/src/constants/index.js +++ b/webapp/src/constants/index.js @@ -7,6 +7,8 @@ export default { export const RHSStates = { PRS: 'pullRequests', REVIEWS: 'reviews', - UNREADS: 'unreads', - ASSIGNMENTS: 'assignments', + TODOS: 'todos', + ISSUES: 'issues', }; + +export const connectUsingBrowserMessage = 'Mattermost desktop client does not support authenticating between Gitlab and Mattermost directly. To connect your Gitlab account with Mattermost, please log in to Mattermost via your web browser and type `/gitlab connect`.'; diff --git a/webapp/src/hooks/index.ts b/webapp/src/hooks/index.ts new file mode 100644 index 00000000..af4882bc --- /dev/null +++ b/webapp/src/hooks/index.ts @@ -0,0 +1,37 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {isDesktopApp} from 'src/utils/user_agent'; +import {connectUsingBrowserMessage} from 'src/constants'; +import {sendEphemeralPost} from '../actions'; +import {Store} from 'redux'; + +type ContextArgs = {channel_id: string}; + +const connectCommand = '/gitlab connect'; + +export default class Hooks { + private store: Store; + + constructor(store: Store) { + this.store = store; + } + + slashCommandWillBePostedHook = (rawMessage: string, contextArgs: ContextArgs) => { + let message; + if (rawMessage) { + message = rawMessage.trim(); + } + + if (!message) { + return Promise.resolve({message, args: contextArgs}); + } + + if (message.startsWith(connectCommand) && isDesktopApp()) { + sendEphemeralPost(connectUsingBrowserMessage)(this.store.dispatch, this.store.getState); + return Promise.resolve({}); + } + + return Promise.resolve({message, args: contextArgs}); + } +} diff --git a/webapp/src/index.js b/webapp/src/index.js index 1a6fb841..1dec0ad0 100644 --- a/webapp/src/index.js +++ b/webapp/src/index.js @@ -7,8 +7,9 @@ import SidebarHeader from './components/sidebar_header'; import TeamSidebar from './components/team_sidebar'; import RHSSidebar from './components/rhs_sidebar'; import UserAttribute from './components/user_attribute'; -import Reducer from './reducers'; import SidebarRight from './components/sidebar_right'; + +import Reducer from './reducers'; import {getConnected, setShowRHSAction} from './actions'; import { handleConnect, @@ -20,6 +21,7 @@ import { import {id} from './manifest'; import Client from './client'; import {getPluginServerRoute} from './selectors'; +import Hooks from './hooks'; let activityFunc; let lastActivityTime = Number.MAX_SAFE_INTEGER; @@ -38,6 +40,9 @@ class PluginClass { registry.registerBottomTeamSidebarComponent(TeamSidebar); registry.registerPopoverUserAttributesComponent(UserAttribute); + const hooks = new Hooks(store); + registry.registerSlashCommandWillBePostedHook(hooks.slashCommandWillBePostedHook); + const {showRHSPlugin} = registry.registerRightHandSidebarComponent(SidebarRight, 'GitLab Plugin'); store.dispatch(setShowRHSAction(() => store.dispatch(showRHSPlugin))); diff --git a/webapp/src/selectors/index.js b/webapp/src/selectors/index.js index f9fbc364..79041778 100644 --- a/webapp/src/selectors/index.js +++ b/webapp/src/selectors/index.js @@ -48,10 +48,10 @@ export const getSidebarData = createSelector( username: pluginState.username, reviewDetails: pluginState.reviewDetails, reviews: mapPrsToDetails(pluginState.lhsData?.reviews, pluginState.reviewDetails), - yourPrs: mapPrsToDetails(pluginState.lhsData?.prs, pluginState.yourPrDetails), + yourAssignedPrs: mapPrsToDetails(pluginState.lhsData?.yourAssignedPrs, pluginState.yourPrDetails), yourPrDetails: pluginState.yourPrDetails, - yourAssignments: pluginState.lhsData?.assignments, - unreads: pluginState.lhsData?.unreads, + yourAssignedIssues: pluginState.lhsData?.yourAssignedIssues, + todos: pluginState.lhsData?.todos, org: pluginState.organization, gitlabURL: pluginState.gitlabURL, rhsState: pluginState.rhsState, diff --git a/webapp/src/utils/user_agent.js b/webapp/src/utils/user_agent.js new file mode 100644 index 00000000..6f980f4e --- /dev/null +++ b/webapp/src/utils/user_agent.js @@ -0,0 +1,3 @@ +const userAgent = window.navigator.userAgent; + +export const isDesktopApp = () => userAgent.indexOf('Mattermost') !== -1 && userAgent.indexOf('Electron') !== -1;