nitdoc: migrate github modalbox to JQuery.UI widget
[nit.git] / share / nitdoc / js / plugins / github.js
index c1fe5d6..92405d9 100644 (file)
  */\r
 define([\r
        "jquery",\r
-       "base64",\r
-       "plugins/github/ui",\r
+       "github-api",\r
+       "plugins/modalbox",\r
        "plugins/github/loginbox",\r
-], function($, Base64, UI, LoginBox) {\r
-       // Load GitHub UI\r
+       "plugins/github/commentbox",\r
+       "utils",\r
+       "Markdown.Converter"\r
+], function($, GithubAPI) {\r
+       var GithubUser = function(login, password, repo, branch) {\r
+               this.login = login;\r
+               this.password = password;\r
+               this.repo = repo;\r
+               this.auth = "Basic " +  (login + ':' + password).base64Encode();\r
+               this.branch = branch;\r
+       }\r
+\r
+       var GithubUI = {\r
+               init: function(upstream, basesha1) {\r
+                       console.info("Github plugin: init GitHub module (upstream: "+ upstream +", base: " + basesha1 + ")");\r
+                       this.origin = this._parseUpstream(upstream);\r
+                       // Add github menu\r
+                       $("nav.main ul").append(\r
+                               $("<li/>")\r
+                               .attr("id", "nitdoc-github-li")\r
+                               .loginbox()\r
+                               .loginbox("displayLogin")\r
+                               .bind("loginbox_logoff", function() {\r
+                                       GithubUI.disactivate();\r
+                               })\r
+                               .bind("loginbox_login", function(event, infos) {\r
+                                       GithubUI._tryLoginFromCredentials(infos);\r
+                               })\r
+                       );\r
+                       // check local session\r
+                       this._tryLoginFromLocalSession();\r
+               },\r
+\r
+               activate: function(user, origin) {\r
+                       this.openedComments = 0;\r
+                       this._saveSession(user);\r
+                       $("#nitdoc-github-li").loginbox("displayLogout", origin, user);\r
+                       this._attachCommentBoxes();\r
+                       this._reloadComments();\r
+\r
+                       // Prevent page unload if there is comments in editing mode\r
+                       $(window).on('beforeunload', function() {\r
+                               if(GithubUI.openedComments > 0){\r
+                                       return "There is uncommited modified comments. Are you sure you want to leave this page?";\r
+                               }\r
+                       });\r
+               },\r
+\r
+               disactivate: function() {\r
+                       if(this.openedComments > 0){\r
+                               if(!confirm('There is uncommited modified comments. Are you sure you want to leave this page?')) {\r
+                                       return false;\r
+                               }\r
+                       }\r
+\r
+                       localStorage.clear();\r
+                       $("#nitdoc-github-li").loginbox("toggle");\r
+                       $("#nitdoc-github-li").loginbox("displayLogin");\r
+                       $(window).unbind('beforeunload');\r
+                       //window.location.reload();\r
+               },\r
+\r
+               /* login */\r
+\r
+               _checkLoginInfos: function(infos) {\r
+                       if(!infos.login || !infos.password || !infos.repo || !infos.branch) {\r
+                               $("<p/>")\r
+                               .text("Please enter your GitHub username, password, repository and branch.")\r
+                               .modalbox({\r
+                                       title: "Sign in error",\r
+                                       isError: true\r
+                               })\r
+                               .modalbox("open");\r
+                               return false;\r
+                       } else {\r
+                               return true;\r
+                       }\r
+               },\r
+\r
+               _tryLoginFromCredentials: function(infos) {\r
+                       if(this._checkLoginInfos(infos)) {\r
+                               var isok = this._tryLogin(infos.login, infos.password, infos.repo, infos.branch);\r
+                               if(isok === true) {\r
+                                       this.activate(this.user, this.origin);\r
+                               } else {\r
+                                       if(isok == "error:login") {\r
+                                               $("<p/>")\r
+                                               .text("The username, password, repo or branch you entered is incorrect.")\r
+                                               .modalbox({\r
+                                                       title: "Github sign in error",\r
+                                                       isError: true\r
+                                               })\r
+                                               .modalbox("open");\r
+                                       } else if(isok == "error:sha") {\r
+                                               $("<p/>")\r
+                                               .text("The provided Github repository must contain the base commit '" + UI.origin.sha + "'.")\r
+                                               .modalbox({\r
+                                                       title: "Github base commit error",\r
+                                                       isError: true\r
+                                               })\r
+                                               .modalbox("open");\r
+                                       } else if(isok == "error:profile") {\r
+                                               $("<p/>")\r
+                                               .text("Please set your public name and email in your " +\r
+                                                       "<a href='https://github.com/settings/profile'>GitHub profile</a>." +\r
+                                                       "<br/><br/>Your public profile informations are used to sign-off your commits.")\r
+                                               .modalbox({\r
+                                                       title: "Github profile error",\r
+                                                       isError: true\r
+                                               })\r
+                                               .modalbox("open");\r
+                                       }\r
+                               }\r
+                       }\r
+               },\r
+\r
+               _tryLoginFromLocalSession: function() {\r
+                       if(localStorage.user) {\r
+                               var session = JSON.parse(localStorage.user);\r
+                               var isok = this._tryLogin(\r
+                                       session.login,\r
+                                       session.password.base64Decode(),\r
+                                       session.repo,\r
+                                       session.branch\r
+                               );\r
+                               if(isok === true) {\r
+                                       this.activate(this.user, this.origin);\r
+                               } else {\r
+                                       console.debug("Github plugin: Session found but authentification failed");\r
+                                       localStorage.clear();\r
+                               }\r
+                       } else {\r
+                               console.debug("Github plugin: No session found");\r
+                       }\r
+               },\r
+\r
+               _tryLogin: function(login, password, repo, branch) {\r
+                       var tmpUser = new GithubUser(login, password, repo, branch);\r
+                       if(!GithubAPI.login(tmpUser)) {\r
+                               return "error:login";\r
+                       }\r
+                       if(!tmpUser.infos.name || !tmpUser.infos.email) {\r
+                               return "error:profile";\r
+                       }\r
+                       var commit = GithubAPI.getCommit(tmpUser, this.origin.sha);\r
+                       if(!commit || !commit.sha) {\r
+                               return "error:sha";\r
+                       }\r
+                       this.user = tmpUser;\r
+                       return true;\r
+               },\r
+\r
+               _saveSession: function(user) {\r
+                       localStorage.user = JSON.stringify({\r
+                               login: user.login,\r
+                               password: user.password.base64Encode(),\r
+                               repo: user.repo,\r
+                               branch: user.branch,\r
+                       });\r
+                       // check local storage synchro with branch\r
+                       if(localStorage.base != this.origin.sha) {\r
+                               console.log("Base changed: cleaned cache");\r
+                               localStorage.requests = "[]";\r
+                               localStorage.base = this.origin.sha;\r
+                       }\r
+               },\r
+\r
+               /* html decoration */\r
+\r
+               // Attach edit button on each comment\r
+               _attachCommentBoxes: function() {\r
+                       $("textarea.baseComment").each(function() {\r
+                               $(this).commentbox();\r
+\r
+                               var isNew = false;\r
+                               if(!$(this).val()) {\r
+                                       isNew = true;\r
+                                       $(this).nextAll(".info:first").find(".noComment").hide()\r
+                                       $(this).nextAll(".info:first").before(\r
+                                               $("<div/>")\r
+                                               .hide()\r
+                                               .addClass("comment")\r
+                                               .append(\r
+                                                       $("<div/>").addClass("nitdoc")\r
+                                               )\r
+                                       )\r
+                               }\r
+\r
+                               $(this).nextAll(".info:first").prepend(\r
+                                       $("<a/>")\r
+                                       .addClass("nitdoc-github-editComment")\r
+                                       .css("cursor", "pointer")\r
+                                       .text((isNew ? "add" : "edit") + " comment")\r
+                                       .click($.proxy(GithubUI._openCommentBox, GithubUI, null, $(this)))\r
+                                       .after(" for ")\r
+                               )\r
+\r
+                               $(this).bind("commentbox_commit", function(event, data) {\r
+                                       GithubUI._saveChanges(data);\r
+                                       $(this).commentbox("close");\r
+                                       GithubUI._reloadComments();\r
+                               })\r
+                               .bind("commentbox_preview", function(event, data) {\r
+                                       var converter = new Markdown.Converter()\r
+                                       var html = converter.makeHtml(data.value);\r
+                                       $("<p/>")\r
+                                       .html(html)\r
+                                       .modalbox({\r
+                                               title: "Preview comment"\r
+                                       })\r
+                                       .modalbox("open");\r
+                               })\r
+                               .bind("commentbox_open", function(event, data) {\r
+                                       GithubUI.openedComments++;\r
+                                       $(this).nextAll(".comment").hide();\r
+                               })\r
+                               .bind("commentbox_close", function(event, data) {\r
+                                       GithubUI.openedComments--;\r
+                                       $(this).nextAll(".comment").show();\r
+                               });\r
+                       });\r
+               },\r
+\r
+               // reload comments from saved pull request\r
+               _reloadComments: function() {\r
+                       if(!localStorage.requests){ return; }\r
+                       $("p.pullRequest").remove();\r
+                       var converter = new Markdown.Converter();\r
+                       var requests = JSON.parse(localStorage.requests);\r
+                       // Look for modified comments in page\r
+                       for(i in requests) {\r
+                               if(!requests[i]) { continue; }\r
+                               var request = requests[i];\r
+                               $("textarea[data-comment-location=\"" + request.location + "\"]").each(function () {\r
+                                       if(request.isClosed) {\r
+                                               var oldComment = request.oldComment.base64Decode();\r
+                                               var htmlComment = converter.makeHtml(oldComment);\r
+                                               $(this).val(oldComment);\r
+                                               if(!$(this).val()) {\r
+                                                       $(this).nextAll("div.comment:first").hide();\r
+                                               } else {\r
+                                                       $(this).nextAll("div.comment:first").show();\r
+                                               }\r
+                                               $(this).nextAll("div.comment").find("div.nitdoc").empty().html(htmlComment);\r
+                                               $(this).nextAll("p.info").find("a.nitdoc-github-editComment").show();\r
+                                       } else {\r
+                                               var newComment = request.comment.base64Decode();\r
+                                               var htmlComment = converter.makeHtml(newComment);\r
+                                               $(this).val(newComment);\r
+                                               if(!$(this).val()) {\r
+                                                       $(this).nextAll("div.comment:first").hide();\r
+                                               } else {\r
+                                                       $(this).nextAll("div.comment:first").show();\r
+                                               }\r
+                                               $(this).nextAll("div.comment").find("div.nitdoc").empty().html(htmlComment);\r
+                                               GithubUI._addPullRequestLink($(this), request);\r
+                                               $(this).nextAll("p.info").find("a.nitdoc-github-editComment").hide();\r
+                                       }\r
+                               });\r
+                       }\r
+               },\r
+\r
+               _addPullRequestLink: function(baseArea, request) {\r
+                       baseArea.nextAll("p.info").before(\r
+                               $("<p/>")\r
+                               .addClass("pullRequest inheritance")\r
+                               .text("comment modified in ")\r
+                               .append(\r
+                                       $("<a/>")\r
+                                       .attr({\r
+                                               href: request.request.html_url,\r
+                                               title: "Review on GitHub"\r
+                                       })\r
+                                       .text("pull request #" + request.request.number)\r
+                               )\r
+                               .append(" ")\r
+                               .append(\r
+                                       $("<a/>")\r
+                                       .data("pullrequest-number", request.request.number)\r
+                                       .addClass("nitdoc-github-update")\r
+                                       .text("update")\r
+                                       .click($.proxy(GithubUI._doUpdateRequest, GithubUI, null, baseArea, request))\r
+                               )\r
+                               .append(" ")\r
+                               .append(\r
+                                       $("<a/>")\r
+                                       .data("pullrequest-number", request.request.number)\r
+                                       .addClass("nitdoc-github-cancel")\r
+                                       .text("cancel")\r
+                                       .click($.proxy(GithubUI._doCancelRequest, GithubUI, null, baseArea, request))\r
+                               )\r
+                       );\r
+               },\r
+\r
+               /* github calls */\r
+\r
+               _saveChanges: function(edit) {\r
+                       // if pull request update close existing pull request for the comment\r
+                       if(edit.requestID) {\r
+                               this._closePullRequest(edit.requestID);\r
+                       }\r
+                       edit.oldContent = this._getFileContent(edit.location.path);\r
+                       edit.newContent = this._mergeComment(edit.oldContent, edit.newComment, edit.location);\r
+                       edit.request = this._pushChanges(edit)\r
+                       if(!edit.request) {\r
+                               $("<p/>")\r
+                               .text("Unable to commit changes.<br/>" + response)\r
+                               .modalbox({\r
+                                       title: "Github commit error",\r
+                                       isError: true\r
+                               })\r
+                               .modalbox("open");\r
+                               return;\r
+                       }\r
+                       this._saveRequest(edit);\r
+               },\r
+\r
+               // save pull request in local storage\r
+               _saveRequest: function(edit) {\r
+                       var requests = {};\r
+                       if(localStorage.requests) {requests = JSON.parse(localStorage.requests)}\r
+                       requests[edit.request.number] = {\r
+                               request: edit.request,\r
+                               location: edit.location.origin,\r
+                               comment: edit.newComment.base64Encode(),\r
+                               oldComment: edit.oldComment.base64Encode()\r
+                       };\r
+                       localStorage.requests = JSON.stringify(requests);\r
+               },\r
+\r
+               /*\r
+                  Creating a new pull request with the new comment take 5 steps:\r
+                       1. get the base tree from latest commit\r
+\r
+                       2. create a new blob with updated file content\r
+                       3. post a new tree from base tree and blob\r
+                       4. post the new commit with new tree\r
+                       5. create the pull request\r
+               */\r
+               _pushChanges: function(edit) {\r
+                       var baseTree = GithubAPI.getTree(this.user, this.origin.sha);\r
+                       if(!baseTree.sha) {\r
+                               $("<p/>")\r
+                               .text("Unable to locate base tree.<br/>" + baseTree.status + ": " + baseTree.statusText)\r
+                               .modalbox({\r
+                                       title: "Github commit error",\r
+                                       isError: true\r
+                               })\r
+                               .modalbox("open");\r
+                               return false;\r
+                       }\r
+                       console.log("Base tree: " + baseTree.url);\r
+                       var newBlob = GithubAPI.createBlob(this.user, edit.newContent);\r
+                       if(!newBlob.sha) {\r
+                               $("<p/>")\r
+                               .text("Unable to create new blob.<br/>" + newBlob.status + ": " + newBlob.statusText)\r
+                               .modalbox({\r
+                                       title: "Github commit error",\r
+                                       isError: true\r
+                               })\r
+                               .modalbox("open");\r
+                               return false;\r
+                       }\r
+                       console.log("New blob: " + newBlob.url);\r
+                       var newTree = GithubAPI.createTree(this.user, baseTree, edit.location.path, newBlob);\r
+                       if(!newTree.sha) {\r
+                               $("<p/>")\r
+                               .text("Unable to create new tree.<br/>" + newTree.status + ": " + newTree.statusText)\r
+                               .modalbox({\r
+                                       title: "Github commit error",\r
+                                       isError: true\r
+                               })\r
+                               .modalbox("open");\r
+                               return false;\r
+                       }\r
+                       console.log("New tree: " + newTree.url);\r
+                       var newCommit = GithubAPI.createCommit(this.user, edit.message, baseTree.sha, newTree);\r
+                       if(!newCommit.sha) {\r
+                               $("<p/>")\r
+                               .text("Unable to create new commit.<br/>" + newCommit.status + ": " + newCommit.statusText)\r
+                               .modalbox({\r
+                                       title: "Github commit error",\r
+                                       isError: true\r
+                               })\r
+                               .modalbox("open");\r
+                               return false;\r
+                       }\r
+                       console.log("New commit: " + newCommit.url);\r
+                       var pullRequest = GithubAPI.createPullRequest(this.user, edit.title, "Pull request from Nitdoc", this.origin, newCommit.sha);\r
+                       if(!pullRequest.number) {\r
+                               $("<p/>")\r
+                               .text("Unable to create pull request.<br/>" + pullRequest.status + ": " + pullRequest.statusText)\r
+                               .modalbox({\r
+                                       title: "Github commit error",\r
+                                       isError: true\r
+                               })\r
+                               .modalbox("open");\r
+                               return false;\r
+                       }\r
+                       console.log("New pull request: " + pullRequest.url);\r
+                       return pullRequest;\r
+               },\r
+\r
+               // close previously opened pull request\r
+               _closePullRequest: function(number) {\r
+                       var requests = JSON.parse(localStorage.requests);\r
+                       if(!requests[number]) {\r
+                               $("<p/>")\r
+                               .text("Unable to close pull request.<br/>" + "Pull request " + number + "not found")\r
+                               .modalbox({\r
+                                       title: "Github commit error",\r
+                                       isError: true\r
+                               })\r
+                               .modalbox("open");\r
+                               return false;\r
+                       }\r
+                       // close pull request\r
+                       var res = GithubAPI.updatePullRequest(this.user, "Closed from Nitdoc", "", "closed", requests[number].request);\r
+                       if(!res.id) {\r
+                               $("<p/>")\r
+                               .text("Unable to close pull request.<br/>" + res.status + ": " + res.statusText)\r
+                               .modalbox({\r
+                                       title: "Github commit error",\r
+                                       isError: true\r
+                               })\r
+                               .modalbox("open");\r
+                               return false;\r
+                       }\r
+                       // update in localstorage\r
+                       requests[number].isClosed = true;\r
+                       localStorage.requests = JSON.stringify(requests);\r
+               },\r
+\r
+               /* internals */\r
+\r
+               _parseUpstream: function(upstream) {\r
+                       var parts = upstream.split(":");\r
+                       return {\r
+                               user: parts[0],\r
+                               repo: parts[1],\r
+                               branch: parts[2],\r
+                               sha: basesha1\r
+                       };\r
+               },\r
+\r
+               _getFileContent: function(githubUrl) {\r
+                       var origFile = GithubAPI.getFile(this.user, githubUrl);\r
+                       if(!origFile.content) {\r
+                               $("<p/>")\r
+                               .text("Unable to locate source file.<br/>" + origFile.status + ": " + origFile.statusText)\r
+                               .modalbox({\r
+                                       title: "Github commit error",\r
+                                       isError: true\r
+                               })\r
+                               .modalbox("open");\r
+                               return;\r
+                       }\r
+                       var base64Content = origFile.content.substring(0, origFile.content.length - 1)\r
+                       return base64Content.base64Decode();\r
+               },\r
+\r
+               _mergeComment: function(fileContent, comment, location) {\r
+                       // replace comment in file content\r
+                       var res = new String();\r
+                       var lines = fileContent.split("\n");\r
+                       // copy lines fron 0 to lstart\r
+                       for(var i = 0; i < location.lstart - 1; i++) {\r
+                               res += lines[i] + "\n";\r
+                       }\r
+                       // set comment\r
+                       if(comment && comment != "") {\r
+                               var commentLines = comment.split("\n");\r
+                               for(var i = 0; i < commentLines.length; i++) {\r
+                                       var line = commentLines[i];\r
+                                       var tab = location.tabpos > 1 ? "\t" : "";\r
+                                       res += tab + (line.length > 0 ? "# " : "#") + line + "\n";\r
+                               }\r
+                       }\r
+                       // copy lines fron lend to end\r
+                       for(var i = location.lend - 1; i < lines.length; i++) {\r
+                               res += lines[i];\r
+                               if(i < lines.length - 1) { res += "\n"; }\r
+                       }\r
+                       return res;\r
+               },\r
+\r
+               /* events */\r
+\r
+               _openCommentBox: function(event, baseArea) {\r
+                       baseArea.commentbox("open", this.user);\r
+               },\r
+\r
+               _doCancelRequest: function(event, baseArea, request) {\r
+                       this._closePullRequest(request.request.number);\r
+                       this._reloadComments();\r
+               },\r
+\r
+               _doUpdateRequest: function(event, baseArea, request) {\r
+                       baseArea.commentbox("open", this.user, request.request.number);\r
+               },\r
+       }\r
+\r
+       // Get github plugin data\r
        var upstream = $("body").attr("data-github-upstream");\r
        var basesha1 = $("body").attr("data-github-base-sha1");\r
        if(upstream && basesha1) {\r
-               console.log("init GitHub module (upstream: "+ upstream +", base: " + basesha1 + ")");\r
-\r
-               // parse origin\r
-               var parts = upstream.split(":");\r
-               UI.origin = {\r
-                       user: parts[0],\r
-                       repo: parts[1],\r
-                       branch: parts[2],\r
-                       sha: basesha1\r
-               };\r
-\r
-               // check local session\r
-               if(localStorage.user) {\r
-\r
-                       var session = JSON.parse(localStorage.user);\r
-                       UI.user = UI.tryLogin(session.login, Base64.decode(session.password), session.repo, session.branch);\r
-                       if(!UI.user.login) {\r
-                               console.log("Session found but authentification failed");\r
-                               localStorage.clear();\r
-                       }\r
-\r
-                       // activate ui\r
-                       LoginBox.init("nav.main ul");\r
-                       if(UI.user && UI.user.login) {\r
-                               LoginBox.displayLogout(UI.origin, UI.user);\r
-                               UI.activate(UI.user);\r
-                       } else {\r
-                               LoginBox.displayLogin();\r
-                       }\r
-               } else {\r
-                       console.log("No session found");\r
-               }\r
+               GithubUI.init(upstream, basesha1);\r
        }\r
 });\r