Skip to content

Commit

Permalink
[GH-61]: Adding "Create Issue" and "Attach Comment to Issue" modals (#…
Browse files Browse the repository at this point in the history
…306)

* [MI-1847] Created new branch for create-issue-modal

* [MI-1847] fixed error while running make check-style

* [MI-1847]Review fixes
1. Converted files to .tsx
2. Defined proper types

* [MI-1847]Fixed initial srtate of milestone in create_issue.tsx

* [MI-1860]Added attach comment to gitlab issue button

* [MI-1860]Added "Attach to gilab issue" modal
1. Button in context menu to open "Attach to gitlab issue" modal.
2. Created modal to attach comment to issue.

* [MI-1860]Attach comment endpoint
1. Created api endpoint to attach comment to an issue.

* [MI-1860]Added post message
1. Added message when comment is created.

* [MI-1860]Handled error
1. Handled error while making call for searching issues.

* Code formatting
1. Converted file to .tsx.
2. Removed redundant code.

* Merge branch 'MI-1847' of github.com:Brightscout/mattermost-plugin-gitlab into MI-1860

* [MI-1860]Pull from MI-1847
1. Fixed error messages.

* removed //eslint-diable...

* [MI-1860]Review fixes
1. Made first letter of error lowercase
2. Used shorthand
3. fixed lint errors

* Merge branch 'MI-1847' of github.com:Brightscout/mattermost-plugin-gitlab into MI-1860
Review fixes:
1. Defined types and converted files to .tsx

* [MI-1860]Review Fixes
1. Defined proper types
2. Changed file extension of types file to .d.ts

* [MI-1860]Fixes
1. Corrected error messages in api.go.
2. Added EOF.

* [MI-1847] Review fixes
1. Moved type "Actions" from index.ts to main file.
2. Converted form_button to fuctional component.
3. Converted gitlab.jsx to .tsx.

* [MI-1847]Review Fixes
1. Converted gitlab.tsx to funtional component.
2. Optional chaining while checking for error in loadoptions function.

* Fixed CI error
1. Added "react-dom" dependency in package.json

* [MI-1847]Fixes
1. Converted class components to functional components.
2. Used absolute paths.

* [MI-1847]Review FIxes
1. Removed mapStateToProps/mapDispatchToProps from functional components and used useSelector/useDispatch.

* [MI-1860]Review fixes
1. Converted functional componets to class components.
2. used useCallback and useMemo hook.
3. Returned proper status codes.
4. Used absolute paths for imports.

* [MI-1847]Review fixes
1. Used BotID for creating post.
2. Used useCallback hooks.
3. Fixed linting errors.

* [MI-1847]Converted remaining class components to functional components

* [MI-1860]Fixed variable names

* [MI-1847]Review fixes
1. Creates a single type for state - "GlobalState".
2. Converted proptypes from interface to type.

* [MI-1847]Review fixes
1. Created a hook for getting assignees, milestones and labels.

* [MI-1847]Review fixes
1. Removed any type.

* [MI-1860]Review fixes
1.Used use callback hook.

* [MI-1860]Review fixes
1. Used useMemo and useCallback.
2. Destructured props.

* [MI-1860]Review Fixes
1. Changed name of a variable.

* [MI-2221] Review fixes (#22)

* [MI-2245] Fix failing "Code scanning / CodeQL" in gitlab create issue modal PR (#23)

[MI-2245] Fix failing "Code scanning / CodeQL" in GitLab create issue modal PR

* Fix lint errors

* [MI-2701]: Done the review fixes of a PR: Adding create issue modal #306 (#28)

* [MI-2701]: Done the review fixes of a PR: Adding create issue modal #306

* [MI-2701]: Review fixes done
1. Used actual gitlab icon

* [MI-2701]: Review fixes done
1. Improved the gitlab icon

* [MI-2701]: Review fixes done
1. Updated the viewbox

* [MI-2947]: Did the review fixes of a gitlab PR #306 (#31)

* [MI-2967]: Did the review fixes of gitlab PR #306 (#33)

* [MI-2967]: Did the review fixes of gitlab PR #306

* [MI-2967]: Improved code readability

* [MI-2967]: Review fixes done
1. Changed the name of a variable

* [MI-2959] Review fixes on Gitlab PR #306(Add create issue modal)
1. Fixed theme issue in post action menu.
2. Fixed the max length of issue title.
3. Added check to display the projects of connected gitlab group only.
4. Added check to display the issues of connected gitlab group only.

* [MI-3787] Fix reported issue:
- Hide the additional fields in create issue modal if the selected priject is removed.

* [MM-3] Removed "ussCallback" and "useMemo" hooks

* [MM-3] Removed useMemo/useCallabck and fixed lint error.

* [MM-3] Separated out form components for create issue modal

* [MM-3] Seperate out form component for attach comment to issue modal

* Fix lint errors

* [MM-158] Review fixes
1. Rename types
2. Create selectors for modal visibility vars
3. Fix warnings in console

* [MM-158] Review fixes:
1. Mode the footer part to form components
2. Create selectors for getting modal contents
3. Make the meesage field in attach comment modal as editable and added validation

* [MM-158] Review fixes:
1. Create a common file for types.
2. Moved the modal form components to modals directory.

* [MM-146] Fixed lint errors

* [MM-289] Fix ci error

* Review fixes: Add new types, error state and some refactoring

* Fix package-lock.json file

* [MM-527] Review fixes

* Create a new type

* Update package-lock.json file due to ci errors

* Fix type errors

---------

Co-authored-by: Nityanand Rai <[email protected]>
Co-authored-by: ayusht2810 <[email protected]>
  • Loading branch information
3 people authored Jul 18, 2024
1 parent 01b5e6c commit c7208c0
Show file tree
Hide file tree
Showing 45 changed files with 16,174 additions and 23,991 deletions.
274 changes: 273 additions & 1 deletion server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/mattermost/mattermost/server/public/plugin"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/bot/logger"
"github.com/mattermost/mattermost/server/public/pluginapi/experimental/flow"
internGitlab "github.com/xanzy/go-gitlab"

"github.com/mattermost/mattermost-plugin-gitlab/server/gitlab"
"github.com/mattermost/mattermost-plugin-gitlab/server/subscription"
Expand All @@ -28,7 +29,9 @@ import (
const (
APIErrorIDNotConnected = "not_connected"

requestTimeout = 30 * time.Second
queryParamSearch = "search"
queryParamProjectID = "projectID"
requestTimeout = 30 * time.Second
)

func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Request) {
Expand All @@ -54,6 +57,13 @@ func (p *Plugin) initializeAPI() {

apiRouter.HandleFunc("/user", p.checkAuth(p.attachContext(p.getGitlabUser), ResponseTypeJSON)).Methods(http.MethodPost)
apiRouter.HandleFunc("/todo", p.checkAuth(p.attachUserContext(p.postToDo), ResponseTypeJSON)).Methods(http.MethodPost)
apiRouter.HandleFunc("/issue", p.checkAuth(p.attachUserContext(p.createIssue), ResponseTypePlain)).Methods(http.MethodPost)
apiRouter.HandleFunc("/attachcommenttoissue", p.checkAuth(p.attachUserContext(p.attachCommentToIssue), ResponseTypePlain)).Methods(http.MethodPost)
apiRouter.HandleFunc("/projects", p.checkAuth(p.attachUserContext(p.getYourProjects), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/labels", p.checkAuth(p.attachUserContext(p.getLabels), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/assignees", p.checkAuth(p.attachUserContext(p.getAssignees), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/milestones", p.checkAuth(p.attachUserContext(p.getMilestones), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/searchissues", p.checkAuth(p.attachUserContext(p.searchIssues), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/lhs-data", p.checkAuth(p.attachUserContext(p.getLHSData), ResponseTypePlain)).Methods(http.MethodGet)
apiRouter.HandleFunc("/prdetails", p.checkAuth(p.attachUserContext(p.getPrDetails), ResponseTypePlain)).Methods(http.MethodPost)
apiRouter.HandleFunc("/issue", p.checkAuth(p.attachUserContext(p.getIssueByNumber), ResponseTypeJSON)).Methods(http.MethodGet)
Expand Down Expand Up @@ -573,6 +583,268 @@ func (p *Plugin) getLHSData(c *UserContext, w http.ResponseWriter, r *http.Reque
p.writeAPIResponse(w, result)
}

func (p *Plugin) createIssue(c *UserContext, w http.ResponseWriter, r *http.Request) {
var issue *gitlab.IssueRequest

if err := json.NewDecoder(r.Body).Decode(&issue); err != nil {
c.Log.WithError(err).Warnf("There was an error while creating the issue")
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("There was an error while creating the issue. Error: %s", err.Error()), StatusCode: http.StatusBadRequest})
return
}

var post *model.Post
var appErr *model.AppError
permalink := ""
if issue.PostID != "" {
post, appErr = p.API.GetPost(issue.PostID)
if appErr != nil {
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load post %s", issue.PostID), StatusCode: http.StatusInternalServerError})
return
}
if post == nil {
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load post %s : not found", issue.PostID), StatusCode: http.StatusNotFound})
return
}
permalink = p.getPermalink(issue.PostID)
}

var result *internGitlab.Issue
err := p.useGitlabClient(c.GitlabInfo, func(info *gitlab.UserInfo, token *oauth2.Token) error {
resp, err := p.GitlabClient.CreateIssue(c.Ctx, c.GitlabInfo, issue, token)
if err != nil {
return err
}
result = resp
return nil
})
if err != nil {
c.Log.WithError(err).Warnf("can't create issue in GitLab")
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("unable to create issue in GitLab. Error: %s", err.Error()), StatusCode: http.StatusInternalServerError})
return
}

rootID := issue.PostID
channelID := issue.ChannelID
message := fmt.Sprintf("Created GitLab issue [#%v](%v)", result.IID, result.WebURL)
if post != nil {
if post.RootId != "" {
rootID = post.RootId
}
channelID = post.ChannelId
message += fmt.Sprintf(" from a [message](%s)", permalink)
}

reply := &model.Post{
Message: message,
ChannelId: channelID,
RootId: rootID,
UserId: p.BotUserID,
}

if post != nil {
_, appErr = p.API.CreatePost(reply)
} else {
p.API.SendEphemeralPost(c.UserID, reply)
}
if appErr != nil {
c.Log.WithError(appErr).Warnf("failed to create notification post")
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to create notification post, postID: %s, channelID: %s", issue.PostID, channelID), StatusCode: http.StatusInternalServerError})
return
}

p.writeAPIResponse(w, result)
}

func (p *Plugin) attachCommentToIssue(c *UserContext, w http.ResponseWriter, r *http.Request) {
var issue *gitlab.IssueRequest

if err := json.NewDecoder(r.Body).Decode(&issue); err != nil {
c.Log.WithError(err).Warnf("There was an error while attaching a comment to the issue")
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("There was an error while attaching a comment to the issue. Error: %s", err.Error()), StatusCode: http.StatusBadRequest})
return
}

if err := p.validateCommentBody(issue); err != nil {
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: err.Error(), StatusCode: http.StatusBadRequest})
return
}

post, appErr := p.API.GetPost(issue.PostID)
if appErr != nil {
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load post %s", issue.PostID), StatusCode: appErr.StatusCode})
return
}
if post == nil {
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to load post %s : not found", issue.PostID), StatusCode: http.StatusNotFound})
return
}

commentUsername, apiErr := p.getUsername(post.UserId)
if apiErr != nil {
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to get username. Error: %s", apiErr.Message), StatusCode: apiErr.StatusCode})
return
}

permalink := p.getPermalink(issue.PostID)

var result *internGitlab.Note
err := p.useGitlabClient(c.GitlabInfo, func(info *gitlab.UserInfo, token *oauth2.Token) error {
resp, err := p.GitlabClient.AttachCommentToIssue(c.Ctx, c.GitlabInfo, issue, permalink, commentUsername, token)
if err != nil {
return err
}
result = resp
return nil
})
if err != nil {
c.Log.WithError(err).Warnf("can't add comment to issue in GitLab")
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("cant't add comment to issue in GitLab. Error: %s", err.Error()), StatusCode: http.StatusInternalServerError})
return
}

rootID := issue.PostID
if post.RootId != "" {
// The original post was a reply
rootID = post.RootId
}

permalinkReplyMessage := fmt.Sprintf("[Message](%s) attached to GitLab issue [#%d](%s)", permalink, issue.IID, issue.WebURL)
reply := &model.Post{
Message: permalinkReplyMessage,
ChannelId: post.ChannelId,
RootId: rootID,
UserId: p.BotUserID,
}

_, appErr = p.API.CreatePost(reply)
if appErr != nil {
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("failed to create notification post %s", issue.PostID), StatusCode: appErr.StatusCode})
return
}
p.writeAPIResponse(w, result)
}

func (p *Plugin) validateCommentBody(issue *gitlab.IssueRequest) error {
if issue.PostID == "" {
return errors.Errorf("please provide a valid post id")
}

if issue.IID == 0 {
return errors.Errorf("please provide a valid post iid")
}

if issue.Comment == "" {
return errors.Errorf("please provide a valid non empty comment")
}
return nil
}

func (p *Plugin) getPermalink(postID string) string {
siteURL := *p.API.GetConfig().ServiceSettings.SiteURL

return fmt.Sprintf("%v/_redirect/pl/%v", siteURL, postID)
}

func (p *Plugin) searchIssues(c *UserContext, w http.ResponseWriter, r *http.Request) {
search := r.FormValue(queryParamSearch)

var result []*internGitlab.Issue
err := p.useGitlabClient(c.GitlabInfo, func(info *gitlab.UserInfo, token *oauth2.Token) error {
resp, err := p.GitlabClient.SearchIssues(c.Ctx, c.GitlabInfo, search, token)
if err != nil {
return err
}
result = resp
return nil
})
if err != nil {
c.Log.WithError(err).Warnf("unable to search issues in GitLab")
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: fmt.Sprintf("unable to search issues in GitLab. Error: %s", err.Error()), StatusCode: http.StatusInternalServerError})
return
}

p.writeAPIResponse(w, result)
}

func (p *Plugin) getYourProjects(c *UserContext, w http.ResponseWriter, r *http.Request) {
var result []*internGitlab.Project
err := p.useGitlabClient(c.GitlabInfo, func(info *gitlab.UserInfo, token *oauth2.Token) error {
resp, err := p.GitlabClient.GetYourProjects(c.Ctx, c.GitlabInfo, token)
if err != nil {
return err
}
result = resp
return nil
})
if err != nil {
c.Log.WithError(err).Warnf("can't list projects in GitLab")
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: "Unable to list projects in GitLab.", StatusCode: http.StatusInternalServerError})
return
}

p.writeAPIResponse(w, result)
}

func (p *Plugin) getLabels(c *UserContext, w http.ResponseWriter, r *http.Request) {
projectID := r.URL.Query().Get(queryParamProjectID)
var result []*internGitlab.Label
err := p.useGitlabClient(c.GitlabInfo, func(info *gitlab.UserInfo, token *oauth2.Token) error {
resp, err := p.GitlabClient.GetLabels(c.Ctx, c.GitlabInfo, projectID, token)
if err != nil {
return err
}
result = resp
return nil
})
if err != nil {
c.Log.WithError(err).Warnf("can't list labels of project in GitLab")
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: "unable to list labels in GitLab.", StatusCode: http.StatusInternalServerError})
return
}

p.writeAPIResponse(w, result)
}

func (p *Plugin) getMilestones(c *UserContext, w http.ResponseWriter, r *http.Request) {
projectID := r.URL.Query().Get(queryParamProjectID)
var result []*internGitlab.Milestone
err := p.useGitlabClient(c.GitlabInfo, func(info *gitlab.UserInfo, token *oauth2.Token) error {
resp, err := p.GitlabClient.GetMilestones(c.Ctx, c.GitlabInfo, projectID, token)
if err != nil {
return err
}
result = resp
return nil
})
if err != nil {
c.Log.WithError(err).Warnf("can't list milestones of project in GitLab")
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: "unable to list milestones in GitLab.", StatusCode: http.StatusInternalServerError})
return
}

p.writeAPIResponse(w, result)
}

func (p *Plugin) getAssignees(c *UserContext, w http.ResponseWriter, r *http.Request) {
projectID := r.URL.Query().Get(queryParamProjectID)
var result []*internGitlab.ProjectMember
err := p.useGitlabClient(c.GitlabInfo, func(info *gitlab.UserInfo, token *oauth2.Token) error {
resp, err := p.GitlabClient.GetProjectMembers(c.Ctx, c.GitlabInfo, projectID, token)
if err != nil {
return err
}
result = resp
return nil
})
if err != nil {
c.Log.WithError(err).Warnf("can't list assignees of the project in GitLab")
p.writeAPIError(w, &APIErrorResponse{ID: "", Message: "unable to list assignees in GitLab.", StatusCode: http.StatusInternalServerError})
return
}

p.writeAPIResponse(w, result)
}

func (p *Plugin) postToDo(c *UserContext, w http.ResponseWriter, r *http.Request) {
_, text, err := p.GetToDo(c.Ctx, c.GitlabInfo)
if err != nil {
Expand Down
29 changes: 29 additions & 0 deletions server/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,12 @@ func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (res
return p.getCommandResponse(args, "Encountered an error getting your todo items."), nil
}
return p.getCommandResponse(args, text), nil
case "issue":
message := p.handleIssue(c, args, parameters)
if message != "" {
p.postCommandResponse(args, message)
}
return &model.CommandResponse{}, nil
case "me":
var gitUser *gitlabLib.User
err := p.useGitlabClient(info, func(info *gitlab.UserInfo, token *oauth2.Token) error {
Expand Down Expand Up @@ -366,6 +372,23 @@ func (p *Plugin) handleSetup(c *plugin.Context, args *model.CommandArgs, paramet
return ""
}

func (p *Plugin) handleIssue(_ *plugin.Context, args *model.CommandArgs, parameters []string) string {
if len(parameters) == 0 {
return "Invalid issue command. Available command is 'create'."
}

command := parameters[0]
parameters = parameters[1:]

switch {
case command == "create":
p.openIssueCreateModal(args.UserId, args.ChannelId, strings.Join(parameters, " "))
return ""
default:
return fmt.Sprintf("This command is not implemented yet. Command: %v", command)
}
}

// webhookCommand processes the /gitlab webhook commands
func (p *Plugin) webhookCommand(ctx context.Context, parameters []string, info *gitlab.UserInfo, enablePrivateRepo bool) string {
if len(parameters) < 1 {
Expand Down Expand Up @@ -808,6 +831,12 @@ func getAutocompleteData(config *configuration) *model.AutocompleteData {
todo := model.NewAutocompleteData("todo", "", "Get a list of todos, assigned issues, assigned merge requests and merge requests awaiting your review")
gitlab.AddCommand(todo)

issue := model.NewAutocompleteData("issue", "[command]", "Available commands: create")
gitlab.AddCommand(issue)

issueCreate := model.NewAutocompleteData("create", "[title]", "Open a dialog to create a new issue in Gitlab, using the title if provided")
issue.AddCommand(issueCreate)

subscriptions := model.NewAutocompleteData("subscriptions", "[command]", "Available commands: Add, List, Delete")

subscriptionsList := model.NewAutocompleteData(commandList, "", "List current channel subscriptions")
Expand Down
Loading

0 comments on commit c7208c0

Please sign in to comment.