From 637f115657c4195e5bff68a603f90142de3b1527 Mon Sep 17 00:00:00 2001 From: Nikhil Ranjan Date: Wed, 11 Sep 2019 11:11:03 +0200 Subject: [PATCH 01/10] added functionality to attach comment to issue --- go.mod | 2 + server/api.go | 117 ++++++++ server/utils.go | 10 + webapp/package-lock.json | 264 ++++++++++++++++-- webapp/package.json | 2 + webapp/src/action_types/index.js | 3 + webapp/src/actions/index.js | 38 +++ webapp/src/client/client.js | 8 + webapp/src/components/form_button.jsx | 63 +++++ .../github_issue_selector.jsx | 159 +++++++++++ .../components/github_issue_selector/index.js | 10 + webapp/src/components/input.jsx | 160 +++++++++++ .../attach_comment_to_issue.jsx | 169 +++++++++++ .../modals/attach_comment_to_issue/index.js | 29 ++ .../attach_comment_to_issue.jsx | 57 ++++ .../attach_comment_to_issue/index.js | 31 ++ webapp/src/components/validator.js | 23 ++ webapp/src/index.js | 5 + webapp/src/reducers/index.js | 24 ++ webapp/src/selectors/index.js | 46 +++ webapp/src/utils/styles.js | 102 +++++++ 21 files changed, 1295 insertions(+), 27 deletions(-) create mode 100644 webapp/src/components/form_button.jsx create mode 100644 webapp/src/components/github_issue_selector/github_issue_selector.jsx create mode 100644 webapp/src/components/github_issue_selector/index.js create mode 100644 webapp/src/components/input.jsx create mode 100644 webapp/src/components/modals/attach_comment_to_issue/attach_comment_to_issue.jsx create mode 100644 webapp/src/components/modals/attach_comment_to_issue/index.js create mode 100644 webapp/src/components/post_menu_actions/attach_comment_to_issue/attach_comment_to_issue.jsx create mode 100644 webapp/src/components/post_menu_actions/attach_comment_to_issue/index.js create mode 100644 webapp/src/components/validator.js create mode 100644 webapp/src/selectors/index.js create mode 100644 webapp/src/utils/styles.js diff --git a/go.mod b/go.mod index fb9d16144..dc2caa78a 100644 --- a/go.mod +++ b/go.mod @@ -32,3 +32,5 @@ require ( // Workaround for https://github.com/golang/go/issues/30831 and fallout. replace github.com/golang/lint => github.com/golang/lint v0.0.0-20190227174305-8f45f776aaf1 + +replace git.apache.org/thrift.git => github.com/apache/thrift v0.12.0 diff --git a/server/api.go b/server/api.go index 869c8976c..95a1cdc07 100644 --- a/server/api.go +++ b/server/api.go @@ -58,8 +58,12 @@ func (p *Plugin) ServeHTTP(c *plugin.Context, w http.ResponseWriter, r *http.Req p.getReviews(w, r) case "/api/v1/yourprs": p.getYourPrs(w, r) + case "/api/v1/searchissues": + p.searchIssues(w, r) case "/api/v1/yourassignments": p.getYourAssignments(w, r) + case "/api/v1/createissuecomment": + p.createIssueComment(w, r) case "/api/v1/mentions": p.getMentions(w, r) case "/api/v1/unreads": @@ -225,6 +229,17 @@ type ConnectedResponse struct { Settings *UserSettings `json:"settings"` } +type CreateIssueCommentRequest struct { + Owner string `json:"owner"` + Repo string `json:"repo"` + Number int `json:"number"` + Comment string `json:"comment"` +} + +type SearchIssueRequest struct { + SearchTerm string `json:"search_term"` +} + type GitHubUserRequest struct { UserID string `json:"user_id"` } @@ -480,6 +495,108 @@ func (p *Plugin) getYourPrs(w http.ResponseWriter, r *http.Request) { w.Write(resp) } +func (p *Plugin) searchIssues(w http.ResponseWriter, r *http.Request) { + config := p.getConfiguration() + + userID := r.Header.Get("Mattermost-User-ID") + if userID == "" { + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + ctx := context.Background() + + var githubClient *github.Client + username := "" + + req := &SearchIssueRequest{} + dec := json.NewDecoder(r.Body) + if err := dec.Decode(&req); err != nil { + if err != nil { + mlog.Error("Error decoding JSON body", mlog.Err(err)) + } + writeAPIError(w, &APIErrorResponse{ID: "", Message: "Please provide a JSON object with search_term key.", StatusCode: http.StatusBadRequest}) + return + } + + if info, err := p.getGitHubUserInfo(userID); err != nil { + writeAPIError(w, err) + return + } else { + githubClient = p.githubConnect(*info.Token) + username = info.GitHubUsername + } + + result, _, err := githubClient.Search.Issues(ctx, getIssuesSearchQuery(username, config.GitHubOrg, req.SearchTerm), &github.SearchOptions{}) + if err != nil { + mlog.Error(err.Error()) + } + + resp, _ := json.Marshal(result.Issues) + w.Write(resp) +} + +func (p *Plugin) createIssueComment(w http.ResponseWriter, r *http.Request) { + userID := r.Header.Get("Mattermost-User-ID") + if userID == "" { + http.Error(w, "Not authorized", http.StatusUnauthorized) + return + } + + req := &CreateIssueCommentRequest{} + dec := json.NewDecoder(r.Body) + if err := dec.Decode(&req); err != nil { + if err != nil { + mlog.Error("Error decoding JSON body", mlog.Err(err)) + } + writeAPIError(w, &APIErrorResponse{ID: "", Message: "Please provide a JSON object.", StatusCode: http.StatusBadRequest}) + return + } + + if req.Owner == "" { + writeAPIError(w, &APIErrorResponse{ID: "", Message: "Please provide a valid repo owner.", StatusCode: http.StatusBadRequest}) + return + } + + if req.Repo == "" { + writeAPIError(w, &APIErrorResponse{ID: "", Message: "Please provide a valid repo.", StatusCode: http.StatusBadRequest}) + return + } + + if req.Number == 0 { + writeAPIError(w, &APIErrorResponse{ID: "", Message: "Please provide a valid issue number.", StatusCode: http.StatusBadRequest}) + return + } + + if req.Comment == "" { + writeAPIError(w, &APIErrorResponse{ID: "", Message: "Please provide a valid non empty comment.", StatusCode: http.StatusBadRequest}) + return + } + + ctx := context.Background() + + var githubClient *github.Client + + if info, err := p.getGitHubUserInfo(userID); err != nil { + writeAPIError(w, err) + return + } else { + githubClient = p.githubConnect(*info.Token) + } + + comment := &github.IssueComment{ + Body: &req.Comment, + } + + result, _, err := githubClient.Issues.CreateComment(ctx, req.Owner, req.Repo, req.Number, comment) + if err != nil { + mlog.Error(err.Error()) + } + + resp, _ := json.Marshal(result) + w.Write(resp) +} + func (p *Plugin) getYourAssignments(w http.ResponseWriter, r *http.Request) { config := p.getConfiguration() diff --git a/server/utils.go b/server/utils.go index 1b7d59b29..20249e007 100644 --- a/server/utils.go +++ b/server/utils.go @@ -29,6 +29,16 @@ func getYourAssigneeSearchQuery(username, org string) string { return buildSearchQuery("is:open assignee:%v archived:false %v", username, org) } +func getIssuesSearchQuery(username, org, searchTerm string) string { + query := "is:open is:issue assignee:%v archived:false %v %v" + orgField := "" + if len(org) != 0 { + orgField = fmt.Sprintf("org:%v", org) + } + + return fmt.Sprintf(query, username, orgField, searchTerm) +} + func buildSearchQuery(query, username, org string) string { orgField := "" if len(org) != 0 { diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 0a4661009..8226bc4c3 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -208,7 +208,6 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", - "dev": true, "requires": { "@babel/types": "^7.0.0" } @@ -1090,7 +1089,6 @@ "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", - "dev": true, "requires": { "esutils": "^2.0.2", "lodash": "^4.17.11", @@ -1100,11 +1098,101 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" + } + } + }, + "@emotion/cache": { + "version": "10.0.17", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.17.tgz", + "integrity": "sha512-442/miwbuwIDfSzfMqZNxuzxSEbskcz/bZ86QBYzEjFrr/oq9w+y5kJY1BHbGhDtr91GO232PZ5NN9XYMwr/Qg==", + "requires": { + "@emotion/sheet": "0.9.3", + "@emotion/stylis": "0.8.4", + "@emotion/utils": "0.11.2", + "@emotion/weak-memoize": "0.2.3" + } + }, + "@emotion/core": { + "version": "10.0.17", + "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.0.17.tgz", + "integrity": "sha512-gykyjjr0sxzVuZBVTVK4dUmYsorc2qLhdYgSiOVK+m7WXgcYTKZevGWZ7TLAgTZvMelCTvhNq8xnf8FR1IdTbg==", + "requires": { + "@babel/runtime": "^7.5.5", + "@emotion/cache": "^10.0.17", + "@emotion/css": "^10.0.14", + "@emotion/serialize": "^0.11.10", + "@emotion/sheet": "0.9.3", + "@emotion/utils": "0.11.2" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.6.0.tgz", + "integrity": "sha512-89eSBLJsxNxOERC0Op4vd+0Bqm6wRMqMbFtV3i0/fbaWw/mJ8Q3eBvgX0G4SyrOOLCtbu98HspF8o09MRT+KzQ==", + "requires": { + "regenerator-runtime": "^0.13.2" + } } } }, + "@emotion/css": { + "version": "10.0.14", + "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.14.tgz", + "integrity": "sha512-MozgPkBEWvorcdpqHZE5x1D/PLEHUitALQCQYt2wayf4UNhpgQs2tN0UwHYS4FMy5ROBH+0ALyCFVYJ/ywmwlg==", + "requires": { + "@emotion/serialize": "^0.11.8", + "@emotion/utils": "0.11.2", + "babel-plugin-emotion": "^10.0.14" + } + }, + "@emotion/hash": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.7.2.tgz", + "integrity": "sha512-RMtr1i6E8MXaBWwhXL3yeOU8JXRnz8GNxHvaUfVvwxokvayUY0zoBeWbKw1S9XkufmGEEdQd228pSZXFkAln8Q==" + }, + "@emotion/memoize": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.2.tgz", + "integrity": "sha512-hnHhwQzvPCW1QjBWFyBtsETdllOM92BfrKWbUTmh9aeOlcVOiXvlPsK4104xH8NsaKfg86PTFsWkueQeUfMA/w==" + }, + "@emotion/serialize": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.10.tgz", + "integrity": "sha512-04AB+wU00vv9jLgkWn13c/GJg2yXp3w7ZR3Q1O6mBSE6mbUmYeNX3OpBhfp//6r47lFyY0hBJJue+bA30iokHQ==", + "requires": { + "@emotion/hash": "0.7.2", + "@emotion/memoize": "0.7.2", + "@emotion/unitless": "0.7.4", + "@emotion/utils": "0.11.2", + "csstype": "^2.5.7" + } + }, + "@emotion/sheet": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.3.tgz", + "integrity": "sha512-c3Q6V7Df7jfwSq5AzQWbXHa5soeE4F5cbqi40xn0CzXxWW9/6Mxq48WJEtqfWzbZtW9odZdnRAkwCQwN12ob4A==" + }, + "@emotion/stylis": { + "version": "0.8.4", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.4.tgz", + "integrity": "sha512-TLmkCVm8f8gH0oLv+HWKiu7e8xmBIaokhxcEKPh1m8pXiV/akCiq50FvYgOwY42rjejck8nsdQxZlXZ7pmyBUQ==" + }, + "@emotion/unitless": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.4.tgz", + "integrity": "sha512-kBa+cDHOR9jpRJ+kcGMsysrls0leukrm68DmFQoMIWQcXdr2cZvyvypWuGYT7U+9kAExUE7+T7r6G3C3A6L8MQ==" + }, + "@emotion/utils": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.2.tgz", + "integrity": "sha512-UHX2XklLl3sIaP6oiMmlVzT0J+2ATTVpf0dHQVyPJHTkOITvXfaSqnRk6mdDhV9pR8T/tHc3cex78IKXssmzrA==" + }, + "@emotion/weak-memoize": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.3.tgz", + "integrity": "sha512-zVgvPwGK7c1aVdUVc9Qv7SqepOGRDrqCw7KZPSZziWGxSlbII3gmvGLPzLX4d0n0BMbamBacUrN22zOMyFFEkQ==" + }, "@webassemblyjs/ast": { "version": "1.8.5", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", @@ -1383,7 +1471,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -1521,6 +1608,38 @@ "pify": "^4.0.1" } }, + "babel-plugin-emotion": { + "version": "10.0.17", + "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.0.17.tgz", + "integrity": "sha512-KNuBadotqYWpQexHhHOu7M9EV1j2c+Oh/JJqBfEQDusD6mnORsCZKHkl+xYwK82CPQ/23wRrsBIEYnKjtbMQJw==", + "requires": { + "@babel/helper-module-imports": "^7.0.0", + "@emotion/hash": "0.7.2", + "@emotion/memoize": "0.7.2", + "@emotion/serialize": "^0.11.10", + "babel-plugin-macros": "^2.0.0", + "babel-plugin-syntax-jsx": "^6.18.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^1.0.5", + "find-root": "^1.1.0", + "source-map": "^0.5.7" + } + }, + "babel-plugin-macros": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.6.1.tgz", + "integrity": "sha512-6W2nwiXme6j1n2erPOnmRiWfObUhWH7Qw1LMi9XZy8cj+KtESu3T6asZvtk5bMQQjX8te35o7CFueiSdL/2NmQ==", + "requires": { + "@babel/runtime": "^7.4.2", + "cosmiconfig": "^5.2.0", + "resolve": "^1.10.0" + } + }, + "babel-plugin-syntax-jsx": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", + "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" + }, "babel-runtime": { "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", @@ -1828,6 +1947,29 @@ "unset-value": "^1.0.0" } }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=" + } + } + }, + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "requires": { + "caller-callsite": "^2.0.0" + } + }, "callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -2067,7 +2209,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -2133,6 +2274,42 @@ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", "dev": true }, + "cosmiconfig": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", + "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", + "requires": { + "import-fresh": "^2.0.0", + "is-directory": "^0.3.1", + "js-yaml": "^3.13.1", + "parse-json": "^4.0.0" + }, + "dependencies": { + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + } + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + } + } + }, "create-ecdh": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", @@ -2227,6 +2404,11 @@ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true }, + "csstype": { + "version": "2.6.6", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz", + "integrity": "sha512-RpFbQGUE74iyPgvr46U9t1xoQBM8T4BL8SxrN66Le2xYAPSaDJJKeztV3awugusb3g3G9iL8StmkBBXhcbbXhg==" + }, "cyclist": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", @@ -2239,6 +2421,11 @@ "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", "dev": true }, + "debounce-promise": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz", + "integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg==" + }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2472,7 +2659,6 @@ "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, "requires": { "is-arrayish": "^0.2.1" } @@ -2505,8 +2691,7 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "eslint": { "version": "5.16.0", @@ -2822,8 +3007,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.0.1", @@ -2852,8 +3036,7 @@ "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "events": { "version": "3.0.0", @@ -3106,8 +3289,7 @@ "find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" }, "find-up": { "version": "3.0.0", @@ -4214,8 +4396,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", - "dev": true + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" }, "is-binary-path": { "version": "1.0.1", @@ -4283,6 +4464,11 @@ } } }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -4428,7 +4614,6 @@ "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4443,8 +4628,7 @@ "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" }, "json-schema-traverse": { "version": "0.4.1", @@ -4703,6 +4887,11 @@ } } }, + "memoize-one": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.1.1.tgz", + "integrity": "sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==" + }, "memory-fs": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.2.0.tgz", @@ -5291,8 +5480,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-type": { "version": "2.0.0", @@ -5669,6 +5857,14 @@ "raf": "^3.1.0" } }, + "react-input-autosize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-2.2.1.tgz", + "integrity": "sha512-3+K4CD13iE4lQQ2WlF8PuV5htfmTRLH6MDnfndHM6LuBRszuXnuyIfE7nhSKt8AzRBZ50bu0sAhkNMeS5pxQQA==", + "requires": { + "prop-types": "^15.5.8" + } + }, "react-is": { "version": "16.8.6", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", @@ -5713,6 +5909,23 @@ "react-is": "^16.8.6" } }, + "react-select": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-3.0.4.tgz", + "integrity": "sha512-fbVISKa/lSUlLsltuatfUiKcWCNvdLXxFFyrzVQCBUsjxJZH/m7UMPdw/ywmRixAmwXAP++MdbNNZypOsiDEfA==", + "requires": { + "@babel/runtime": "^7.4.4", + "@emotion/cache": "^10.0.9", + "@emotion/core": "^10.0.9", + "@emotion/css": "^10.0.9", + "classnames": "^2.2.5", + "memoize-one": "^5.0.0", + "prop-types": "^15.6.0", + "raf": "^3.4.0", + "react-input-autosize": "^2.2.1", + "react-transition-group": "^2.2.1" + } + }, "react-transition-group": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.9.0.tgz", @@ -5938,7 +6151,6 @@ "version": "1.11.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.0.tgz", "integrity": "sha512-WL2pBDjqT6pGUNSUzMw00o4T7If+z4H2x3Gz893WoUQ5KW8Vr9txp00ykiP16VBaZF5+j/OcXJHZ9+PCvdiDKw==", - "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -6315,8 +6527,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-resolve": { "version": "0.5.2", @@ -6381,8 +6592,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "ssri": { "version": "6.0.1", diff --git a/webapp/package.json b/webapp/package.json index 6ed3ba610..15dea28b7 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -9,12 +9,14 @@ "fix": "eslint --ignore-pattern node_modules --ignore-pattern dist --ext .js --ext .jsx . --quiet --fix" }, "dependencies": { + "debounce-promise": "3.1.2", "mattermost-redux": "5.11.0", "prop-types": "15.7.2", "react": "16.8.6", "react-bootstrap": "0.32.4", "react-custom-scrollbars": "^4.2.1", "react-redux": "7.0.3", + "react-select": "3.0.4", "redux": "4.0.1", "superagent": "5.0.5" }, diff --git a/webapp/src/action_types/index.js b/webapp/src/action_types/index.js index 9b567b775..65cf73343 100644 --- a/webapp/src/action_types/index.js +++ b/webapp/src/action_types/index.js @@ -10,4 +10,7 @@ export default { RECEIVED_GITHUB_USER: pluginId + '_received_github_user', RECEIVED_SHOW_RHS_ACTION: pluginId + '_received_rhs_action', UPDATE_RHS_STATE: pluginId + '_update_rhs_state', + CLOSE_ATTACH_COMMENT_TO_ISSUE_MODAL: pluginId + '_close_attach_modal', + OPEN_ATTACH_COMMENT_TO_ISSUE_MODAL: pluginId + '_open_attach_modal', + RECEIVED_ATTACH_COMMENT_RESULT: pluginId + '_received_attach_comment', }; diff --git a/webapp/src/actions/index.js b/webapp/src/actions/index.js index ea7620bc8..1009f07ba 100644 --- a/webapp/src/actions/index.js +++ b/webapp/src/actions/index.js @@ -210,3 +210,41 @@ export function updateRhsState(rhsState) { state: rhsState, }; } + +export function openAttachCommentToIssueModal(postId) { + return { + type: ActionTypes.OPEN_ATTACH_COMMENT_TO_ISSUE_MODAL, + data: { + postId, + }, + }; +} + +export function closeAttachCommentToIssueModal() { + return { + type: ActionTypes.CLOSE_ATTACH_COMMENT_TO_ISSUE_MODAL, + }; +} + +export function attachCommentToIssue(payload) { + return async (dispatch, getState) => { + let data; + try { + data = await Client.attachCommentToIssue(payload); + } catch (error) { + return {error}; + } + + const connected = await checkAndHandleNotConnected(data)(dispatch, getState); + if (!connected) { + return {error: data}; + } + + dispatch({ + type: ActionTypes.RECEIVED_ATTACH_COMMENT_RESULT, + data, + }); + + return {data}; + }; +} diff --git a/webapp/src/client/client.js b/webapp/src/client/client.js index 2a3f13a77..743615c2f 100644 --- a/webapp/src/client/client.js +++ b/webapp/src/client/client.js @@ -33,6 +33,14 @@ export default class Client { return this.doPost(`${this.url}/user`, {user_id: userID}); } + searchIssues = async (searchTerm) => { + return this.doPost(`${this.url}/searchissues`, {searchTerm}); + } + + attachCommentToIssue = async (payload) => { + return this.doPost(`${this.url}/createissuecomment`, payload); + } + doGet = async (url, body, headers = {}) => { headers['X-Requested-With'] = 'XMLHttpRequest'; headers['X-Timezone-Offset'] = new Date().getTimezoneOffset(); diff --git a/webapp/src/components/form_button.jsx b/webapp/src/components/form_button.jsx new file mode 100644 index 000000000..d993c27ce --- /dev/null +++ b/webapp/src/components/form_button.jsx @@ -0,0 +1,63 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; + +export default class FormButton extends PureComponent { + static propTypes = { + executing: PropTypes.bool, + disabled: PropTypes.bool, + executingMessage: PropTypes.node, + defaultMessage: PropTypes.node, + btnClass: PropTypes.string, + extraClasses: PropTypes.string, + saving: PropTypes.bool, + savingMessage: PropTypes.string, + type: PropTypes.string, + }; + + static defaultProps = { + disabled: false, + savingMessage: 'Creating', + defaultMessage: 'Create', + btnClass: 'btn-primary', + extraClasses: '', + }; + + render() { + const {saving, disabled, savingMessage, defaultMessage, btnClass, extraClasses, ...props} = this.props; + + let contents; + if (saving) { + contents = ( + + + {savingMessage} + + ); + } else { + contents = defaultMessage; + } + + let className = 'save-button btn ' + btnClass; + + if (extraClasses) { + className += ' ' + extraClasses; + } + + return ( + + ); + } +} diff --git a/webapp/src/components/github_issue_selector/github_issue_selector.jsx b/webapp/src/components/github_issue_selector/github_issue_selector.jsx new file mode 100644 index 000000000..ff394d97c --- /dev/null +++ b/webapp/src/components/github_issue_selector/github_issue_selector.jsx @@ -0,0 +1,159 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {Component} from 'react'; +import PropTypes from 'prop-types'; + +import debounce from 'debounce-promise'; +import AsyncSelect from 'react-select/async'; + +import {getStyleForReactSelect} from 'utils/styles'; +import {searchIssues} from 'client'; + +const searchDebounceDelay = 400; + +export default class JiraIssueSelector extends Component { + static propTypes = { + id: PropTypes.string, + required: PropTypes.bool, + theme: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, + error: PropTypes.string, + value: PropTypes.object, + addValidate: PropTypes.func.isRequired, + removeValidate: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + + this.state = {invalid: false}; + } + + componentDidMount() { + if (this.props.addValidate && this.props.id) { + this.props.addValidate(this.props.id, this.isValid); + } + } + + componentWillUnmount() { + if (this.props.removeValidate && this.props.id) { + this.props.removeValidate(this.props.id); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevState.invalid && this.props.value !== prevProps.value) { + this.setState({invalid: false}); //eslint-disable-line react/no-did-update-set-state + } + } + + handleIssueSearchTermChange = (inputValue) => { + return this.debouncedSearchIssues(inputValue); + }; + + searchIssues = (text) => { + const textEncoded = encodeURIComponent(text.trim().replace(/"/g, '\\"')); + + return searchIssues(textEncoded).then((data) => { + console.log(data); //eslint-disable-line + return data; + }).catch((e) => { + this.setState({error: e}); + }); + }; + + debouncedSearchIssues = debounce(this.searchIssues, searchDebounceDelay); + + onChange = (e) => { + const value = e ? e.value : ''; + this.props.onChange(value); + } + + isValid = () => { + if (!this.props.required) { + return true; + } + + const valid = this.props.value && this.props.value.toString().length !== 0; + this.setState({invalid: !valid}); + return valid; + }; + + render = () => { + const {error} = this.props; + const requiredStar = ( + + {'*'} + + ); + + let issueError = null; + if (error) { + issueError = ( +

+ {error} +

+ ); + } + + const serverError = this.state.error; + let errComponent; + if (this.state.error) { + errComponent = ( +

+ + {serverError.toString()} +

+ ); + } + + const requiredMsg = 'This field is required.'; + let validationError = null; + if (this.props.required && this.state.invalid) { + validationError = ( +

+ {requiredMsg} +

+ ); + } + + return ( +
+ {errComponent} + + {this.props.required && requiredStar} + + {validationError} + {issueError} +
+ {'Returns issues sorted by most recently updated.'}
+
+
+ ); + } +} diff --git a/webapp/src/components/github_issue_selector/index.js b/webapp/src/components/github_issue_selector/index.js new file mode 100644 index 000000000..9569370a5 --- /dev/null +++ b/webapp/src/components/github_issue_selector/index.js @@ -0,0 +1,10 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {connect} from 'react-redux'; + +import GithubIssueSelector from './github_issue_selector'; + +const mapStateToProps = () => ({}); + +export default connect(mapStateToProps, null, null, {withRef: true})(GithubIssueSelector); diff --git a/webapp/src/components/input.jsx b/webapp/src/components/input.jsx new file mode 100644 index 000000000..14cbdd63d --- /dev/null +++ b/webapp/src/components/input.jsx @@ -0,0 +1,160 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import React, {PureComponent} from 'react'; +import PropTypes from 'prop-types'; + +import Setting from './setting.jsx'; + +export default class Input extends PureComponent { + static propTypes = { + id: PropTypes.string, + label: PropTypes.node.isRequired, + placeholder: PropTypes.string, + helpText: PropTypes.node, + value: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number, + ]), + addValidate: PropTypes.func.isRequired, + removeValidate: PropTypes.func.isRequired, + maxLength: PropTypes.number, + onChange: PropTypes.func, + disabled: PropTypes.bool, + required: PropTypes.bool, + readOnly: PropTypes.bool, + type: PropTypes.oneOf([ + 'number', + 'input', + 'textarea', + ]), + }; + + static defaultProps = { + type: 'input', + maxLength: null, + required: false, + readOnly: false, + }; + + constructor(props) { + super(props); + + this.state = {invalid: false}; + } + + componentDidMount() { + if (this.props.addValidate && this.props.id) { + this.props.addValidate(this.props.id, this.isValid); + } + } + + componentWillUnmount() { + if (this.props.removeValidate && this.props.id) { + this.props.removeValidate(this.props.id); + } + } + + componentDidUpdate(prevProps, prevState) { + if (prevState.invalid && this.props.value !== prevProps.value) { + this.setState({invalid: false}); //eslint-disable-line react/no-did-update-set-state + } + } + + handleChange = (e) => { + if (this.props.type === 'number') { + this.props.onChange(this.props.id, parseInt(e.target.value, 10)); + } else { + this.props.onChange(this.props.id, e.target.value); + } + }; + + isValid = () => { + if (!this.props.required) { + return true; + } + const valid = this.props.value && this.props.value.toString().length !== 0; + this.setState({invalid: !valid}); + return valid; + }; + + render() { + const requiredMsg = 'This field is required.'; + const style = getStyle(); + const value = this.props.value || ''; + + let validationError = null; + if (this.props.required && this.state.invalid) { + validationError = ( +

+ {requiredMsg} +

+ ); + } + + let input = null; + if (this.props.type === 'input') { + input = ( + + ); + } else if (this.props.type === 'number') { + input = ( + + ); + } else if (this.props.type === 'textarea') { + input = ( +