From ce79eaada80a9210c636c6a22a248dd6c76c54d2 Mon Sep 17 00:00:00 2001 From: bati11 Date: Sat, 5 Apr 2014 20:31:30 +0900 Subject: [PATCH 1/6] Add escapeTaskList method, it escapse '- [] ' characters --- src/main/scala/view/Markdown.scala | 5 ++ .../view/GitBucketHtmlSerializerSpec.scala | 65 +++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala index 4d998bd27..c347d14ad 100644 --- a/src/main/scala/view/Markdown.scala +++ b/src/main/scala/view/Markdown.scala @@ -9,6 +9,7 @@ import org.pegdown.ast._ import org.pegdown.LinkRenderer.Rendering import java.text.Normalizer import java.util.Locale +import java.util.regex.Pattern import scala.collection.JavaConverters._ import service.{RequestCache, WikiService} @@ -149,4 +150,8 @@ object GitBucketHtmlSerializer { val noSpecialChars = StringUtil.urlEncode(normalized) noSpecialChars.toLowerCase(Locale.ENGLISH) } + + def escapeTaskList(text: String): String = { + Pattern.compile("""^ *- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("task:$1: ") + } } diff --git a/src/test/scala/view/GitBucketHtmlSerializerSpec.scala b/src/test/scala/view/GitBucketHtmlSerializerSpec.scala index 946b62629..5b49c886a 100644 --- a/src/test/scala/view/GitBucketHtmlSerializerSpec.scala +++ b/src/test/scala/view/GitBucketHtmlSerializerSpec.scala @@ -25,4 +25,69 @@ class GitBucketHtmlSerializerSpec extends Specification { after mustEqual "foo%21bar%40baz%3e9000" } } + + "escapeTaskList" should { + "convert '- [ ] ' to 'task: :'" in { + val before = "- [ ] aaaa" + val after = escapeTaskList(before) + after mustEqual "task: : aaaa" + } + + "convert ' - [ ] ' to 'task: :'" in { + val before = " - [ ] aaaa" + val after = escapeTaskList(before) + after mustEqual "task: : aaaa" + } + + "convert only first '- [ ] '" in { + val before = " - [ ] aaaa - [ ] bbb" + val after = escapeTaskList(before) + after mustEqual "task: : aaaa - [ ] bbb" + } + + "convert '- [x] ' to 'task: :'" in { + val before = " - [x] aaaa" + val after = escapeTaskList(before) + after mustEqual "task:x: aaaa" + } + + "convert multi lines" in { + val before = """ +tasks +- [x] aaaa +- [ ] bbb +""" + val after = escapeTaskList(before) + after mustEqual """ +tasks +task:x: aaaa +task: : bbb +""" + } + + "no convert if inserted before '- [ ] '" in { + val before = " a - [ ] aaaa" + val after = escapeTaskList(before) + after mustEqual " a - [ ] aaaa" + } + + "no convert '- [] '" in { + val before = " - [] aaaa" + val after = escapeTaskList(before) + after mustEqual " - [] aaaa" + } + + "no convert '- [ ]a'" in { + val before = " - [ ]a aaaa" + val after = escapeTaskList(before) + after mustEqual " - [ ]a aaaa" + } + + "no convert '-[ ] '" in { + val before = " -[ ] aaaa" + val after = escapeTaskList(before) + after mustEqual " -[ ] aaaa" + } + } } + From 843722f82ef4c911cff81db1357b2ca2b260368d Mon Sep 17 00:00:00 2001 From: bati11 Date: Sun, 6 Apr 2014 00:45:19 +0900 Subject: [PATCH 2/6] Implement the feature "Task List" --- src/main/scala/app/IssuesController.scala | 4 +- .../app/RepositoryViewerController.scala | 3 +- src/main/scala/view/Markdown.scala | 29 +++++++++--- src/main/scala/view/helpers.scala | 4 +- src/main/twirl/helper/preview.scala.html | 5 ++- src/main/twirl/issues/commentform.scala.html | 4 +- src/main/twirl/issues/commentlist.scala.html | 45 +++++++++++++++++-- src/main/twirl/issues/create.scala.html | 2 +- src/main/twirl/issues/issuedetail.scala.html | 40 ++++++++++++++++- src/main/twirl/pulls/compare.scala.html | 2 +- src/main/twirl/wiki/edit.scala.html | 4 +- 11 files changed, 117 insertions(+), 25 deletions(-) diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala index be564ee39..b8f168208 100644 --- a/src/main/scala/app/IssuesController.scala +++ b/src/main/scala/app/IssuesController.scala @@ -186,7 +186,7 @@ trait IssuesControllerBase extends ControllerBase { org.json4s.jackson.Serialization.write( Map("title" -> x.title, "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", - repository, false, true) + repository, false, true, true) )) } } else Unauthorized @@ -203,7 +203,7 @@ trait IssuesControllerBase extends ControllerBase { contentType = formats("json") org.json4s.jackson.Serialization.write( Map("content" -> view.Markdown.toHtml(x.content, - repository, false, true) + repository, false, true, true) )) } } else Unauthorized diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index dda0e6147..c38eb011e 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -30,7 +30,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { contentType = "text/html" view.helpers.markdown(params("content"), repository, params("enableWikiLink").toBoolean, - params("enableRefsLink").toBoolean) + params("enableRefsLink").toBoolean, + params("enableTaskList").toBoolean) }) /** diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala index c347d14ad..f83ef10d7 100644 --- a/src/main/scala/view/Markdown.scala +++ b/src/main/scala/view/Markdown.scala @@ -11,7 +11,7 @@ import java.text.Normalizer import java.util.Locale import java.util.regex.Pattern import scala.collection.JavaConverters._ -import service.{RequestCache, WikiService} +import service.{RepositoryService, RequestCache, WikiService} object Markdown { @@ -19,17 +19,22 @@ object Markdown { * Converts Markdown of Wiki pages to HTML. */ def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): String = { + enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false)(implicit context: app.Context): String = { // escape issue id - val source = if(enableRefsLink){ + val s = if(enableRefsLink){ markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2") } else markdown + // escape task list + val source = if(enableTaskList){ + GitBucketHtmlSerializer.escapeTaskList(s) + } else s + val rootNode = new PegDownProcessor( Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS ).parseMarkdown(source.toCharArray) - new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode) + new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList).toHtml(rootNode) } } @@ -83,11 +88,12 @@ class GitBucketHtmlSerializer( markdown: String, repository: service.RepositoryService.RepositoryInfo, enableWikiLink: Boolean, - enableRefsLink: Boolean + enableRefsLink: Boolean, + enableTaskList: Boolean )(implicit val context: app.Context) extends ToHtmlSerializer( new GitBucketLinkRender(context, repository, enableWikiLink), Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava - ) with LinkConverter with RequestCache { + ) with RepositoryService with LinkConverter with RequestCache { override protected def printImageTag(imageNode: SuperNode, url: String): Unit = printer.print("\"").printEncoded(printChildrenToString(imageNode)).print("\"/") @@ -130,7 +136,10 @@ class GitBucketHtmlSerializer( override def visit(node: TextNode): Unit = { // convert commit id and username to link. - val text = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText + val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText + + // convert task list to checkbox. + val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission(repository.owner, repository.name, context.loginAccount)) else t if (abbreviations.isEmpty) { printer.print(text) @@ -154,4 +163,10 @@ object GitBucketHtmlSerializer { def escapeTaskList(text: String): String = { Pattern.compile("""^ *- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("task:$1: ") } + + def convertCheckBox(text: String, hasWritePermission: Boolean): String = { + val disabled = if (hasWritePermission) "" else "disabled" + text.replaceAll("task:x:", """") + .replaceAll("task: :", """") + } } diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index feb5f21fb..774c0346b 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -41,8 +41,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache * Converts Markdown of Wiki pages to HTML. */ def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = - Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) + enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false)(implicit context: app.Context): Html = + Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList)) def renderMarkup(filePath: List[String], fileContent: String, branch: String, repository: service.RepositoryService.RepositoryInfo, diff --git a/src/main/twirl/helper/preview.scala.html b/src/main/twirl/helper/preview.scala.html index cc6504bdc..1586128de 100644 --- a/src/main/twirl/helper/preview.scala.html +++ b/src/main/twirl/helper/preview.scala.html @@ -1,4 +1,4 @@ -@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, +@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context) @import context._ @import view.helpers._ @@ -36,7 +36,8 @@ $(function(){ $.post('@url(repository)/_preview', { content : $('#content').val(), enableWikiLink : @enableWikiLink, - enableRefsLink : @enableRefsLink + enableRefsLink : @enableRefsLink, + enableTaskList : @enableTaskList }, function(data){ $('#preview-area').html(data); prettyPrint(); diff --git a/src/main/twirl/issues/commentform.scala.html b/src/main/twirl/issues/commentform.scala.html index edf656eaf..e4a6836c8 100644 --- a/src/main/twirl/issues/commentform.scala.html +++ b/src/main/twirl/issues/commentform.scala.html @@ -8,7 +8,7 @@
@avatar(loginAccount.get.userName, 48)
- @helper.html.preview(repository, "", false, true, "width: 680px; height: 100px; max-height: 150px;", elastic = true) + @helper.html.preview(repository, "", false, true, true, "width: 680px; height: 100px; max-height: 150px;", elastic = true)
@@ -26,4 +26,4 @@ $(function(){ $('').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form'); }); }); - \ No newline at end of file + diff --git a/src/main/twirl/issues/commentlist.scala.html b/src/main/twirl/issues/commentlist.scala.html index 92a82f75c..0c3399aef 100644 --- a/src/main/twirl/issues/commentlist.scala.html +++ b/src/main/twirl/issues/commentlist.scala.html @@ -30,7 +30,7 @@ @if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){ @defining(comment.content.substring(comment.content.length - 40)){ id => - @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true) + @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true) } } else { @if(comment.action == "refer"){ @@ -38,7 +38,7 @@ Issue #@issueId: @rest.mkString(":") } } else { - @markdown(comment.content, repository, false, true) + @markdown(comment.content, repository, false, true, true) } }
@@ -109,5 +109,44 @@ $(function(){ } return false; }); + + var extractMarkdown = function(data){ + $('body').append('
'); + $('#tmp').html(data); + var markdown = $('#tmp textarea').val(); + $('#tmp').remove(); + return markdown; + }; + + $('div[id^=commentContent-').on('click', ':checkbox', function(ev){ + var $commentContent = $(ev.target).parents('div[id^=commentContent-]'), + commentId = $commentContent.attr('id').replace(/commentContent-/, ''), + checkboxes = $commentContent.find(':checkbox'); + $.get('@url(repository)/issue_comments/_data/' + commentId, + { + dataType : 'html' + }, + function(data){ + var ss = [], + markdown = extractMarkdown(data), + xs = markdown.split(/- \[[x| ]\]/g); + for (var i=0; i \ No newline at end of file + diff --git a/src/main/twirl/issues/create.scala.html b/src/main/twirl/issues/create.scala.html index b82884c5b..b91456ace 100644 --- a/src/main/twirl/issues/create.scala.html +++ b/src/main/twirl/issues/create.scala.html @@ -56,7 +56,7 @@
- @helper.html.preview(repository, "", false, true, "width: 600px; height: 200px; max-height: 250px;", elastic = true) + @helper.html.preview(repository, "", false, true, true, "width: 600px; height: 200px; max-height: 250px;", elastic = true)
diff --git a/src/main/twirl/issues/issuedetail.scala.html b/src/main/twirl/issues/issuedetail.scala.html index df678d1ef..d6975619f 100644 --- a/src/main/twirl/issues/issuedetail.scala.html +++ b/src/main/twirl/issues/issuedetail.scala.html @@ -77,7 +77,7 @@
- @markdown(issue.content getOrElse "No description given.", repository, false, true) + @markdown(issue.content getOrElse "No description given.", repository, false, true, true)
@@ -141,5 +141,41 @@ $(function(){ } }); }); + + var extractMarkdown = function(data){ + $('body').append('
'); + $('#tmp').html(data); + var markdown = $('#tmp textarea').val(); + $('#tmp').remove(); + return markdown; + }; + + $('#issueContent').on('click', ':checkbox', function(ev){ + var checkboxes = $('#issueContent :checkbox'); + $.get('@url(repository)/issues/_data/@issue.issueId', + { + dataType : 'html' + }, + function(data){ + var ss = [], + markdown = extractMarkdown(data), + xs = markdown.split(/- \[[x| ]\]/g); + for (var i=0; i \ No newline at end of file + diff --git a/src/main/twirl/pulls/compare.scala.html b/src/main/twirl/pulls/compare.scala.html index 92fc8f813..ddf679e0d 100644 --- a/src/main/twirl/pulls/compare.scala.html +++ b/src/main/twirl/pulls/compare.scala.html @@ -58,7 +58,7 @@
- @helper.html.preview(repository, "", false, true, "width: 600px; height: 200px;") + @helper.html.preview(repository, "", false, true, true, "width: 600px; height: 200px;") diff --git a/src/main/twirl/wiki/edit.scala.html b/src/main/twirl/wiki/edit.scala.html index d75ccbb39..9e7c5fc56 100644 --- a/src/main/twirl/wiki/edit.scala.html +++ b/src/main/twirl/wiki/edit.scala.html @@ -23,7 +23,7 @@
- @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 900px; height: 400px;", "") + @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, "width: 900px; height: 400px;", "") @@ -36,4 +36,4 @@ $(function(){ return confirm('Are you sure you want to delete this page?'); }); }); - \ No newline at end of file + From b55fc649a6443ddde5f916568aa12eaab7c0f59d Mon Sep 17 00:00:00 2001 From: bati11 Date: Fri, 19 Sep 2014 12:42:06 +0900 Subject: [PATCH 3/6] Change crlf to lf --- src/main/scala/app/IssuesController.scala | 806 +++++++++++----------- src/main/twirl/issues/create.scala.html | 294 ++++---- 2 files changed, 550 insertions(+), 550 deletions(-) diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala index 547abf281..8f0320ef0 100644 --- a/src/main/scala/app/IssuesController.scala +++ b/src/main/scala/app/IssuesController.scala @@ -1,403 +1,403 @@ -package app - -import jp.sf.amateras.scalatra.forms._ - -import service._ -import IssuesService._ -import util._ -import util.Implicits._ -import util.ControlUtil._ -import org.scalatra.Ok -import model.Issue - -class IssuesController extends IssuesControllerBase - with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator - -trait IssuesControllerBase extends ControllerBase { - self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService - with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator => - - case class IssueCreateForm(title: String, content: Option[String], - assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) - case class IssueEditForm(title: String, content: Option[String]) - case class CommentForm(issueId: Int, content: String) - case class IssueStateForm(issueId: Int, content: Option[String]) - - val issueCreateForm = mapping( - "title" -> trim(label("Title", text(required))), - "content" -> trim(optional(text())), - "assignedUserName" -> trim(optional(text())), - "milestoneId" -> trim(optional(number())), - "labelNames" -> trim(optional(text())) - )(IssueCreateForm.apply) - - val issueEditForm = mapping( - "title" -> trim(label("Title", text(required))), - "content" -> trim(optional(text())) - )(IssueEditForm.apply) - - val commentForm = mapping( - "issueId" -> label("Issue Id", number()), - "content" -> trim(label("Comment", text(required))) - )(CommentForm.apply) - - val issueStateForm = mapping( - "issueId" -> label("Issue Id", number()), - "content" -> trim(optional(text())) - )(IssueStateForm.apply) - - get("/:owner/:repository/issues")(referrersOnly { - searchIssues("all", _) - }) - - get("/:owner/:repository/issues/assigned/:userName")(referrersOnly { - searchIssues("assigned", _) - }) - - get("/:owner/:repository/issues/created_by/:userName")(referrersOnly { - searchIssues("created_by", _) - }) - - get("/:owner/:repository/issues/:id")(referrersOnly { repository => - defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) => - getIssue(owner, name, issueId) map { - issues.html.issue( - _, - getComments(owner, name, issueId.toInt), - getIssueLabels(owner, name, issueId.toInt), - (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, - getMilestonesWithIssueCount(owner, name), - getLabels(owner, name), - hasWritePermission(owner, name, context.loginAccount), - repository) - } getOrElse NotFound - } - }) - - get("/:owner/:repository/issues/new")(readableUsersOnly { repository => - defining(repository.owner, repository.name){ case (owner, name) => - issues.html.create( - (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, - getMilestones(owner, name), - getLabels(owner, name), - hasWritePermission(owner, name, context.loginAccount), - repository) - } - }) - - post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - val writable = hasWritePermission(owner, name, context.loginAccount) - val userName = context.loginAccount.get.userName - - // insert issue - val issueId = createIssue(owner, name, userName, form.title, form.content, - if(writable) form.assignedUserName else None, - if(writable) form.milestoneId else None) - - // insert labels - if(writable){ - form.labelNames.map { value => - val labels = getLabels(owner, name) - value.split(",").foreach { labelName => - labels.find(_.labelName == labelName).map { label => - registerIssueLabel(owner, name, issueId, label.labelId) - } - } - } - } - - // record activity - recordCreateIssueActivity(owner, name, userName, issueId, form.title) - - // extract references and create refer comment - getIssue(owner, name, issueId.toString).foreach { issue => - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) - } - - // notifications - Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ - Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") - } - - redirect(s"/${owner}/${name}/issues/${issueId}") - } - }) - - ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - getIssue(owner, name, params("id")).map { issue => - if(isEditable(owner, name, issue.openedUserName)){ - // update issue - updateIssue(owner, name, issue.issueId, form.title, form.content) - // extract references and create refer comment - createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) - - redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") - } else Unauthorized - } getOrElse NotFound - } - }) - - post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => - redirect(s"/${repository.owner}/${repository.name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") - } getOrElse NotFound - }) - - post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => - handleComment(form.issueId, form.content, repository)() map { case (issue, id) => - redirect(s"/${repository.owner}/${repository.name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") - } getOrElse NotFound - }) - - ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => - defining(repository.owner, repository.name){ case (owner, name) => - getComment(owner, name, params("id")).map { comment => - if(isEditable(owner, name, comment.commentedUserName)){ - updateComment(comment.commentId, form.content) - redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") - } else Unauthorized - } getOrElse NotFound - } - }) - - ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository => - defining(repository.owner, repository.name){ case (owner, name) => - getComment(owner, name, params("id")).map { comment => - if(isEditable(owner, name, comment.commentedUserName)){ - Ok(deleteComment(comment.commentId)) - } else Unauthorized - } getOrElse NotFound - } - }) - - ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => - getIssue(repository.owner, repository.name, params("id")) map { x => - if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ - params.get("dataType") collect { - case t if t == "html" => issues.html.editissue( - x.title, x.content, x.issueId, x.userName, x.repositoryName) - } getOrElse { - contentType = formats("json") - org.json4s.jackson.Serialization.write( - Map("title" -> x.title, - "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", - repository, false, true, true) - )) - } - } else Unauthorized - } getOrElse NotFound - }) - - ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => - getComment(repository.owner, repository.name, params("id")) map { x => - if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ - params.get("dataType") collect { - case t if t == "html" => issues.html.editcomment( - x.content, x.commentId, x.userName, x.repositoryName) - } getOrElse { - contentType = formats("json") - org.json4s.jackson.Serialization.write( - Map("content" -> view.Markdown.toHtml(x.content, - repository, false, true, true) - )) - } - } else Unauthorized - } getOrElse NotFound - }) - - ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => - defining(params("id").toInt){ issueId => - registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) - issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) - } - }) - - ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => - defining(params("id").toInt){ issueId => - deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) - issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) - } - }) - - ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => - updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) - Ok("updated") - }) - - ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => - updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId")) - milestoneId("milestoneId").map { milestoneId => - getMilestonesWithIssueCount(repository.owner, repository.name) - .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => - issues.milestones.html.progress(openCount + closeCount, closeCount, false) - } getOrElse NotFound - } getOrElse Ok() - }) - - post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => - defining(params.get("value")){ action => - executeBatch(repository) { - handleComment(_, None, repository)( _ => action) - } - } - }) - - post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => - params("value").toIntOpt.map{ labelId => - executeBatch(repository) { issueId => - getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { - registerIssueLabel(repository.owner, repository.name, issueId, labelId) - } - } - } getOrElse NotFound - }) - - post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => - defining(assignedUserName("value")){ value => - executeBatch(repository) { - updateAssignedUserName(repository.owner, repository.name, _, value) - } - } - }) - - post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => - defining(milestoneId("value")){ value => - executeBatch(repository) { - updateMilestoneId(repository.owner, repository.name, _, value) - } - } - }) - - get("/:owner/:repository/_attached/:file")(referrersOnly { repository => - (Directory.getAttachedDir(repository.owner, repository.name) match { - case dir if(dir.exists && dir.isDirectory) => - dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => - contentType = FileUtil.getMimeType(file.getName) - file - } - case _ => None - }) getOrElse NotFound - }) - - val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") - val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) - - private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean = - hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName - - private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { - params("checked").split(',') map(_.toInt) foreach execute - redirect(s"/${repository.owner}/${repository.name}/issues") - } - - private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { - StringUtil.extractIssueId(message).foreach { issueId => - if(getIssue(owner, repository, issueId).isDefined){ - createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, - fromIssue.issueId + ":" + fromIssue.title, "refer") - } - } - } - - /** - * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] - */ - private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) - (getAction: model.Issue => Option[String] = - p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { - - defining(repository.owner, repository.name){ case (owner, name) => - val userName = context.loginAccount.get.userName - - getIssue(owner, name, issueId.toString) map { issue => - val (action, recordActivity) = - getAction(issue) - .collect { - case "close" => true -> (Some("close") -> - Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) - case "reopen" => false -> (Some("reopen") -> - Some(recordReopenIssueActivity _)) - } - .map { case (closed, t) => - updateClosed(owner, name, issueId, closed) - t - } - .getOrElse(None -> None) - - val commentId = content - .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") ) - .getOrElse ( action.get.capitalize -> action.get ) - match { - case (content, action) => createComment(owner, name, userName, issueId, content, action) - } - - // record activity - content foreach { - (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) - (owner, name, userName, issueId, _) - } - recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) - - // extract references and create refer comment - content.map { content => - createReferComment(owner, name, issue, content) - } - - // notifications - Notifier() match { - case f => - content foreach { - f.toNotify(repository, issueId, _){ - Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ - if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}") - } - } - action foreach { - f.toNotify(repository, issueId, _){ - Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") - } - } - } - - issue -> commentId - } - } - } - - private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { - defining(repository.owner, repository.name){ case (owner, repoName) => - val filterUser = Map(filter -> params.getOrElse("userName", "")) - val page = IssueSearchCondition.page(request) - val sessionKey = Keys.Session.Issues(owner, repoName) - - // retrieve search condition - val condition = session.putAndGet(sessionKey, - if(request.hasQueryString) IssueSearchCondition(request) - else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) - ) - - issues.html.list( - searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), - page, - (getCollaborators(owner, repoName) :+ owner).sorted, - getMilestones(owner, repoName), - getLabels(owner, repoName), - countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), - countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), - countIssue(condition, Map.empty, false, owner -> repoName), - context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)), - context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)), - countIssueGroupByLabels(owner, repoName, condition, filterUser), - condition, - filter, - repository, - hasWritePermission(owner, repoName, context.loginAccount)) - } - } - -} +package app + +import jp.sf.amateras.scalatra.forms._ + +import service._ +import IssuesService._ +import util._ +import util.Implicits._ +import util.ControlUtil._ +import org.scalatra.Ok +import model.Issue + +class IssuesController extends IssuesControllerBase + with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator + +trait IssuesControllerBase extends ControllerBase { + self: IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService + with ReadableUsersAuthenticator with ReferrerAuthenticator with CollaboratorsAuthenticator => + + case class IssueCreateForm(title: String, content: Option[String], + assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) + case class IssueEditForm(title: String, content: Option[String]) + case class CommentForm(issueId: Int, content: String) + case class IssueStateForm(issueId: Int, content: Option[String]) + + val issueCreateForm = mapping( + "title" -> trim(label("Title", text(required))), + "content" -> trim(optional(text())), + "assignedUserName" -> trim(optional(text())), + "milestoneId" -> trim(optional(number())), + "labelNames" -> trim(optional(text())) + )(IssueCreateForm.apply) + + val issueEditForm = mapping( + "title" -> trim(label("Title", text(required))), + "content" -> trim(optional(text())) + )(IssueEditForm.apply) + + val commentForm = mapping( + "issueId" -> label("Issue Id", number()), + "content" -> trim(label("Comment", text(required))) + )(CommentForm.apply) + + val issueStateForm = mapping( + "issueId" -> label("Issue Id", number()), + "content" -> trim(optional(text())) + )(IssueStateForm.apply) + + get("/:owner/:repository/issues")(referrersOnly { + searchIssues("all", _) + }) + + get("/:owner/:repository/issues/assigned/:userName")(referrersOnly { + searchIssues("assigned", _) + }) + + get("/:owner/:repository/issues/created_by/:userName")(referrersOnly { + searchIssues("created_by", _) + }) + + get("/:owner/:repository/issues/:id")(referrersOnly { repository => + defining(repository.owner, repository.name, params("id")){ case (owner, name, issueId) => + getIssue(owner, name, issueId) map { + issues.html.issue( + _, + getComments(owner, name, issueId.toInt), + getIssueLabels(owner, name, issueId.toInt), + (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, + getMilestonesWithIssueCount(owner, name), + getLabels(owner, name), + hasWritePermission(owner, name, context.loginAccount), + repository) + } getOrElse NotFound + } + }) + + get("/:owner/:repository/issues/new")(readableUsersOnly { repository => + defining(repository.owner, repository.name){ case (owner, name) => + issues.html.create( + (getCollaborators(owner, name) ::: (if(getAccountByUserName(owner).get.isGroupAccount) Nil else List(owner))).sorted, + getMilestones(owner, name), + getLabels(owner, name), + hasWritePermission(owner, name, context.loginAccount), + repository) + } + }) + + post("/:owner/:repository/issues/new", issueCreateForm)(readableUsersOnly { (form, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + val writable = hasWritePermission(owner, name, context.loginAccount) + val userName = context.loginAccount.get.userName + + // insert issue + val issueId = createIssue(owner, name, userName, form.title, form.content, + if(writable) form.assignedUserName else None, + if(writable) form.milestoneId else None) + + // insert labels + if(writable){ + form.labelNames.map { value => + val labels = getLabels(owner, name) + value.split(",").foreach { labelName => + labels.find(_.labelName == labelName).map { label => + registerIssueLabel(owner, name, issueId, label.labelId) + } + } + } + } + + // record activity + recordCreateIssueActivity(owner, name, userName, issueId, form.title) + + // extract references and create refer comment + getIssue(owner, name, issueId.toString).foreach { issue => + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) + } + + // notifications + Notifier().toNotify(repository, issueId, form.content.getOrElse("")){ + Notifier.msgIssue(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") + } + + redirect(s"/${owner}/${name}/issues/${issueId}") + } + }) + + ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + getIssue(owner, name, params("id")).map { issue => + if(isEditable(owner, name, issue.openedUserName)){ + // update issue + updateIssue(owner, name, issue.issueId, form.title, form.content) + // extract references and create refer comment + createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) + + redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") + } else Unauthorized + } getOrElse NotFound + } + }) + + post("/:owner/:repository/issue_comments/new", commentForm)(readableUsersOnly { (form, repository) => + handleComment(form.issueId, Some(form.content), repository)() map { case (issue, id) => + redirect(s"/${repository.owner}/${repository.name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + } getOrElse NotFound + }) + + post("/:owner/:repository/issue_comments/state", issueStateForm)(readableUsersOnly { (form, repository) => + handleComment(form.issueId, form.content, repository)() map { case (issue, id) => + redirect(s"/${repository.owner}/${repository.name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${form.issueId}#comment-${id}") + } getOrElse NotFound + }) + + ajaxPost("/:owner/:repository/issue_comments/edit/:id", commentForm)(readableUsersOnly { (form, repository) => + defining(repository.owner, repository.name){ case (owner, name) => + getComment(owner, name, params("id")).map { comment => + if(isEditable(owner, name, comment.commentedUserName)){ + updateComment(comment.commentId, form.content) + redirect(s"/${owner}/${name}/issue_comments/_data/${comment.commentId}") + } else Unauthorized + } getOrElse NotFound + } + }) + + ajaxPost("/:owner/:repository/issue_comments/delete/:id")(readableUsersOnly { repository => + defining(repository.owner, repository.name){ case (owner, name) => + getComment(owner, name, params("id")).map { comment => + if(isEditable(owner, name, comment.commentedUserName)){ + Ok(deleteComment(comment.commentId)) + } else Unauthorized + } getOrElse NotFound + } + }) + + ajaxGet("/:owner/:repository/issues/_data/:id")(readableUsersOnly { repository => + getIssue(repository.owner, repository.name, params("id")) map { x => + if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ + params.get("dataType") collect { + case t if t == "html" => issues.html.editissue( + x.title, x.content, x.issueId, x.userName, x.repositoryName) + } getOrElse { + contentType = formats("json") + org.json4s.jackson.Serialization.write( + Map("title" -> x.title, + "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", + repository, false, true, true) + )) + } + } else Unauthorized + } getOrElse NotFound + }) + + ajaxGet("/:owner/:repository/issue_comments/_data/:id")(readableUsersOnly { repository => + getComment(repository.owner, repository.name, params("id")) map { x => + if(isEditable(x.userName, x.repositoryName, x.commentedUserName)){ + params.get("dataType") collect { + case t if t == "html" => issues.html.editcomment( + x.content, x.commentId, x.userName, x.repositoryName) + } getOrElse { + contentType = formats("json") + org.json4s.jackson.Serialization.write( + Map("content" -> view.Markdown.toHtml(x.content, + repository, false, true, true) + )) + } + } else Unauthorized + } getOrElse NotFound + }) + + ajaxPost("/:owner/:repository/issues/:id/label/new")(collaboratorsOnly { repository => + defining(params("id").toInt){ issueId => + registerIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) + issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) + } + }) + + ajaxPost("/:owner/:repository/issues/:id/label/delete")(collaboratorsOnly { repository => + defining(params("id").toInt){ issueId => + deleteIssueLabel(repository.owner, repository.name, issueId, params("labelId").toInt) + issues.html.labellist(getIssueLabels(repository.owner, repository.name, issueId)) + } + }) + + ajaxPost("/:owner/:repository/issues/:id/assign")(collaboratorsOnly { repository => + updateAssignedUserName(repository.owner, repository.name, params("id").toInt, assignedUserName("assignedUserName")) + Ok("updated") + }) + + ajaxPost("/:owner/:repository/issues/:id/milestone")(collaboratorsOnly { repository => + updateMilestoneId(repository.owner, repository.name, params("id").toInt, milestoneId("milestoneId")) + milestoneId("milestoneId").map { milestoneId => + getMilestonesWithIssueCount(repository.owner, repository.name) + .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => + issues.milestones.html.progress(openCount + closeCount, closeCount, false) + } getOrElse NotFound + } getOrElse Ok() + }) + + post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => + defining(params.get("value")){ action => + executeBatch(repository) { + handleComment(_, None, repository)( _ => action) + } + } + }) + + post("/:owner/:repository/issues/batchedit/label")(collaboratorsOnly { repository => + params("value").toIntOpt.map{ labelId => + executeBatch(repository) { issueId => + getIssueLabel(repository.owner, repository.name, issueId, labelId) getOrElse { + registerIssueLabel(repository.owner, repository.name, issueId, labelId) + } + } + } getOrElse NotFound + }) + + post("/:owner/:repository/issues/batchedit/assign")(collaboratorsOnly { repository => + defining(assignedUserName("value")){ value => + executeBatch(repository) { + updateAssignedUserName(repository.owner, repository.name, _, value) + } + } + }) + + post("/:owner/:repository/issues/batchedit/milestone")(collaboratorsOnly { repository => + defining(milestoneId("value")){ value => + executeBatch(repository) { + updateMilestoneId(repository.owner, repository.name, _, value) + } + } + }) + + get("/:owner/:repository/_attached/:file")(referrersOnly { repository => + (Directory.getAttachedDir(repository.owner, repository.name) match { + case dir if(dir.exists && dir.isDirectory) => + dir.listFiles.find(_.getName.startsWith(params("file") + ".")).map { file => + contentType = FileUtil.getMimeType(file.getName) + file + } + case _ => None + }) getOrElse NotFound + }) + + val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") + val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) + + private def isEditable(owner: String, repository: String, author: String)(implicit context: app.Context): Boolean = + hasWritePermission(owner, repository, context.loginAccount) || author == context.loginAccount.get.userName + + private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { + params("checked").split(',') map(_.toInt) foreach execute + redirect(s"/${repository.owner}/${repository.name}/issues") + } + + private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { + StringUtil.extractIssueId(message).foreach { issueId => + if(getIssue(owner, repository, issueId).isDefined){ + createComment(owner, repository, context.loginAccount.get.userName, issueId.toInt, + fromIssue.issueId + ":" + fromIssue.title, "refer") + } + } + } + + /** + * @see [[https://github.com/takezoe/gitbucket/wiki/CommentAction]] + */ + private def handleComment(issueId: Int, content: Option[String], repository: RepositoryService.RepositoryInfo) + (getAction: model.Issue => Option[String] = + p1 => params.get("action").filter(_ => isEditable(p1.userName, p1.repositoryName, p1.openedUserName))) = { + + defining(repository.owner, repository.name){ case (owner, name) => + val userName = context.loginAccount.get.userName + + getIssue(owner, name, issueId.toString) map { issue => + val (action, recordActivity) = + getAction(issue) + .collect { + case "close" => true -> (Some("close") -> + Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) + case "reopen" => false -> (Some("reopen") -> + Some(recordReopenIssueActivity _)) + } + .map { case (closed, t) => + updateClosed(owner, name, issueId, closed) + t + } + .getOrElse(None -> None) + + val commentId = content + .map ( _ -> action.map( _ + "_comment" ).getOrElse("comment") ) + .getOrElse ( action.get.capitalize -> action.get ) + match { + case (content, action) => createComment(owner, name, userName, issueId, content, action) + } + + // record activity + content foreach { + (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) + (owner, name, userName, issueId, _) + } + recordActivity foreach ( _ (owner, name, userName, issueId, issue.title) ) + + // extract references and create refer comment + content.map { content => + createReferComment(owner, name, issue, content) + } + + // notifications + Notifier() match { + case f => + content foreach { + f.toNotify(repository, issueId, _){ + Notifier.msgComment(s"${context.baseUrl}/${owner}/${name}/${ + if(issue.isPullRequest) "pull" else "issues"}/${issueId}#comment-${commentId}") + } + } + action foreach { + f.toNotify(repository, issueId, _){ + Notifier.msgStatus(s"${context.baseUrl}/${owner}/${name}/issues/${issueId}") + } + } + } + + issue -> commentId + } + } + } + + private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { + defining(repository.owner, repository.name){ case (owner, repoName) => + val filterUser = Map(filter -> params.getOrElse("userName", "")) + val page = IssueSearchCondition.page(request) + val sessionKey = Keys.Session.Issues(owner, repoName) + + // retrieve search condition + val condition = session.putAndGet(sessionKey, + if(request.hasQueryString) IssueSearchCondition(request) + else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) + ) + + issues.html.list( + searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), + page, + (getCollaborators(owner, repoName) :+ owner).sorted, + getMilestones(owner, repoName), + getLabels(owner, repoName), + countIssue(condition.copy(state = "open"), filterUser, false, owner -> repoName), + countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), + countIssue(condition, Map.empty, false, owner -> repoName), + context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)), + context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)), + countIssueGroupByLabels(owner, repoName, condition, filterUser), + condition, + filter, + repository, + hasWritePermission(owner, repoName, context.loginAccount)) + } + } + +} diff --git a/src/main/twirl/issues/create.scala.html b/src/main/twirl/issues/create.scala.html index b2d18bddd..0df9385c1 100644 --- a/src/main/twirl/issues/create.scala.html +++ b/src/main/twirl/issues/create.scala.html @@ -1,147 +1,147 @@ -@(collaborators: List[String], - milestones: List[model.Milestone], - labels: List[model.Label], - hasWritePermission: Boolean, - repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) -@import context._ -@import view.helpers._ -@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ - @html.menu("issues", repository){ - @tab("", true, repository) - -
-
-
@avatar(loginAccount.get.userName, 48)
-
-
- - -
- No one is assigned - @if(hasWritePermission){ - - @helper.html.dropdown() { -
  • Clear assignee
  • - @collaborators.map { collaborator => -
  • @avatar(collaborator, 20) @collaborator
  • - } - } - } -
    - No milestone - @if(hasWritePermission){ - - @helper.html.dropdown() { -
  • No milestone
  • - @milestones.filter(_.closedDate.isEmpty).map { milestone => -
  • - - @milestone.title -
    - @milestone.dueDate.map { dueDate => - @if(isPast(dueDate)){ - Due in @date(dueDate) - } else { - Due in @date(dueDate) - } - }.getOrElse { - No due date - } -
    -
    -
  • - } - } - } -
    -
    -
    - @helper.html.preview(repository, "", false, true, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true) -
    -
    -
    - -
    -
    -
    - @if(hasWritePermission){ - Add Labels -
    -
    - - -
    -
    - } -
    -
    - - } -} - +@(collaborators: List[String], + milestones: List[model.Milestone], + labels: List[model.Label], + hasWritePermission: Boolean, + repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) +@import context._ +@import view.helpers._ +@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ + @html.menu("issues", repository){ + @tab("", true, repository) +
    +
    +
    +
    @avatar(loginAccount.get.userName, 48)
    +
    +
    + + +
    + No one is assigned + @if(hasWritePermission){ + + @helper.html.dropdown() { +
  • Clear assignee
  • + @collaborators.map { collaborator => +
  • @avatar(collaborator, 20) @collaborator
  • + } + } + } +
    + No milestone + @if(hasWritePermission){ + + @helper.html.dropdown() { +
  • No milestone
  • + @milestones.filter(_.closedDate.isEmpty).map { milestone => +
  • + + @milestone.title +
    + @milestone.dueDate.map { dueDate => + @if(isPast(dueDate)){ + Due in @date(dueDate) + } else { + Due in @date(dueDate) + } + }.getOrElse { + No due date + } +
    +
    +
  • + } + } + } +
    +
    +
    + @helper.html.preview(repository, "", false, true, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true) +
    +
    +
    + +
    +
    +
    + @if(hasWritePermission){ + Add Labels +
    +
    + + +
    +
    + } +
    +
    +
    + } +} + From e1f310317dec70e152bf39f466fe049609a0b232 Mon Sep 17 00:00:00 2001 From: bati11 Date: Fri, 19 Sep 2014 14:13:53 +0900 Subject: [PATCH 4/6] Modify GitBucketHtmlSerializer constructor parameters - Add to the GitBucketHtmlSerializer constructor parameter "hasWritePermission" - Remove the call to the RepositoryService.hasWritePermission in GitBucketHtmlSerializer --- src/main/scala/app/IssuesController.scala | 4 ++-- .../scala/app/RepositoryViewerController.scala | 3 ++- src/main/scala/view/Markdown.scala | 14 ++++++++------ src/main/scala/view/helpers.scala | 4 ++-- src/main/twirl/helper/preview.scala.html | 2 +- src/main/twirl/issues/commentform.scala.html | 2 +- src/main/twirl/issues/commentlist.scala.html | 4 ++-- src/main/twirl/issues/create.scala.html | 2 +- src/main/twirl/issues/issuedetail.scala.html | 2 +- src/main/twirl/pulls/compare.scala.html | 2 +- src/main/twirl/wiki/edit.scala.html | 2 +- 11 files changed, 22 insertions(+), 19 deletions(-) diff --git a/src/main/scala/app/IssuesController.scala b/src/main/scala/app/IssuesController.scala index 3fb59ae05..4fe478ef4 100644 --- a/src/main/scala/app/IssuesController.scala +++ b/src/main/scala/app/IssuesController.scala @@ -187,7 +187,7 @@ trait IssuesControllerBase extends ControllerBase { org.json4s.jackson.Serialization.write( Map("title" -> x.title, "content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", - repository, false, true, true) + repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName)) )) } } else Unauthorized @@ -204,7 +204,7 @@ trait IssuesControllerBase extends ControllerBase { contentType = formats("json") org.json4s.jackson.Serialization.write( Map("content" -> view.Markdown.toHtml(x.content, - repository, false, true, true) + repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName)) )) } } else Unauthorized diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index 18cff36c0..18a9f3fef 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -78,7 +78,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { view.helpers.markdown(params("content"), repository, params("enableWikiLink").toBoolean, params("enableRefsLink").toBoolean, - params("enableTaskList").toBoolean) + params("enableTaskList").toBoolean, + hasWritePermission(repository.owner, repository.name, context.loginAccount)) }) /** diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala index 90a00ffa2..e7c9cd660 100644 --- a/src/main/scala/view/Markdown.scala +++ b/src/main/scala/view/Markdown.scala @@ -11,7 +11,7 @@ import java.text.Normalizer import java.util.Locale import java.util.regex.Pattern import scala.collection.JavaConverters._ -import service.{RepositoryService, RequestCache, WikiService} +import service.{RequestCache, WikiService} object Markdown { @@ -19,7 +19,8 @@ object Markdown { * Converts Markdown of Wiki pages to HTML. */ def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false)(implicit context: app.Context): String = { + enableWikiLink: Boolean, enableRefsLink: Boolean, + enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): String = { // escape issue id val s = if(enableRefsLink){ markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2") @@ -34,7 +35,7 @@ object Markdown { Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS ).parseMarkdown(source.toCharArray) - new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList).toHtml(rootNode) + new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode) } } @@ -89,11 +90,12 @@ class GitBucketHtmlSerializer( repository: service.RepositoryService.RepositoryInfo, enableWikiLink: Boolean, enableRefsLink: Boolean, - enableTaskList: Boolean + enableTaskList: Boolean, + hasWritePermission: Boolean )(implicit val context: app.Context) extends ToHtmlSerializer( new GitBucketLinkRender(context, repository, enableWikiLink), Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava - ) with RepositoryService with LinkConverter with RequestCache { + ) with LinkConverter with RequestCache { override protected def printImageTag(imageNode: SuperNode, url: String): Unit = printer.print("") @@ -147,7 +149,7 @@ class GitBucketHtmlSerializer( val t = if(enableRefsLink) convertRefsLinks(node.getText, repository, "issue:") else node.getText // convert task list to checkbox. - val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission(repository.owner, repository.name, context.loginAccount)) else t + val text = if(enableTaskList) GitBucketHtmlSerializer.convertCheckBox(t, hasWritePermission) else t if (abbreviations.isEmpty) { printer.print(text) diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index 73433bb83..7c11f1bc0 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -48,8 +48,8 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache * Converts Markdown of Wiki pages to HTML. */ def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, - enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false)(implicit context: app.Context): Html = - Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList)) + enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html = + Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission)) def renderMarkup(filePath: List[String], fileContent: String, branch: String, repository: service.RepositoryService.RepositoryInfo, diff --git a/src/main/twirl/helper/preview.scala.html b/src/main/twirl/helper/preview.scala.html index 2b04c896a..3c0e13ead 100644 --- a/src/main/twirl/helper/preview.scala.html +++ b/src/main/twirl/helper/preview.scala.html @@ -1,4 +1,4 @@ -@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, +@(repository: service.RepositoryService.RepositoryInfo, content: String, enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean, hasWritePermission: Boolean, style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context) @import context._ @import view.helpers._ diff --git a/src/main/twirl/issues/commentform.scala.html b/src/main/twirl/issues/commentform.scala.html index 67a532c5f..edabb5d36 100644 --- a/src/main/twirl/issues/commentform.scala.html +++ b/src/main/twirl/issues/commentform.scala.html @@ -9,7 +9,7 @@
    @avatar(loginAccount.get.userName, 48)
    - @helper.html.preview(repository, "", false, true, true, "width: 635px; height: 100px; max-height: 150px;", elastic = true) + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 635px; height: 100px; max-height: 150px;", elastic = true)
    diff --git a/src/main/twirl/issues/commentlist.scala.html b/src/main/twirl/issues/commentlist.scala.html index 1605c23c7..171e71267 100644 --- a/src/main/twirl/issues/commentlist.scala.html +++ b/src/main/twirl/issues/commentlist.scala.html @@ -30,7 +30,7 @@ @if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){ @defining(comment.content.substring(comment.content.length - 40)){ id => - @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true) + @markdown(comment.content.substring(0, comment.content.length - 41), repository, false, true, true, hasWritePermission) } } else { @if(comment.action == "refer"){ @@ -38,7 +38,7 @@ Issue #@issueId: @rest.mkString(":") } } else { - @markdown(comment.content, repository, false, true, true) + @markdown(comment.content, repository, false, true, true, hasWritePermission) } }
    diff --git a/src/main/twirl/issues/create.scala.html b/src/main/twirl/issues/create.scala.html index 3d8faad64..bcb234c25 100644 --- a/src/main/twirl/issues/create.scala.html +++ b/src/main/twirl/issues/create.scala.html @@ -56,7 +56,7 @@

    - @helper.html.preview(repository, "", false, true, true, "width: 565px; height: 200px; max-height: 250px;", elastic = true) + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 565px; height: 200px; max-height: 250px;", elastic = true)
    diff --git a/src/main/twirl/issues/issuedetail.scala.html b/src/main/twirl/issues/issuedetail.scala.html index 2bab9570f..e8c4e27f9 100644 --- a/src/main/twirl/issues/issuedetail.scala.html +++ b/src/main/twirl/issues/issuedetail.scala.html @@ -77,7 +77,7 @@
    - @markdown(issue.content getOrElse "No description given.", repository, false, true, true) + @markdown(issue.content getOrElse "No description given.", repository, false, true, true, hasWritePermission)
    diff --git a/src/main/twirl/pulls/compare.scala.html b/src/main/twirl/pulls/compare.scala.html index 0ffaab7df..7d900abb9 100644 --- a/src/main/twirl/pulls/compare.scala.html +++ b/src/main/twirl/pulls/compare.scala.html @@ -58,7 +58,7 @@
    - @helper.html.preview(repository, "", false, true, true, "width: 580px; height: 200px;") + @helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 580px; height: 200px;") diff --git a/src/main/twirl/wiki/edit.scala.html b/src/main/twirl/wiki/edit.scala.html index f3fbdaf80..36a61dc12 100644 --- a/src/main/twirl/wiki/edit.scala.html +++ b/src/main/twirl/wiki/edit.scala.html @@ -22,7 +22,7 @@
    - @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, "width: 850px; height: 400px;", "") + @helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, false, "width: 850px; height: 400px;", "") From 26b14ded58e24a7f859b1ab9e7eb5a5edddf409b Mon Sep 17 00:00:00 2001 From: bati11 Date: Sat, 20 Sep 2014 10:57:33 +0900 Subject: [PATCH 5/6] Add nested task list support --- src/main/scala/view/Markdown.scala | 28 +++++++++++++++++-- .../webapp/assets/common/css/gitbucket.css | 14 ++++++++++ .../view/GitBucketHtmlSerializerSpec.scala | 18 ++++++------ 3 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/main/scala/view/Markdown.scala b/src/main/scala/view/Markdown.scala index e7c9cd660..d06dc1f6d 100644 --- a/src/main/scala/view/Markdown.scala +++ b/src/main/scala/view/Markdown.scala @@ -157,6 +157,28 @@ class GitBucketHtmlSerializer( printWithAbbreviations(text) } } + + override def visit(node: BulletListNode): Unit = { + if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { + printer.println().print("""
      """).indent(+2) + visitChildren(node) + printer.indent(-2).println().print("
    ") + } else { + printIndentedTag(node, "ul") + } + } + + override def visit(node: ListItemNode): Unit = { + if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) { + printer.println() + printer.print("""
  • """) + visitChildren(node) + printer.print("
  • ") + } else { + printer.println() + printTag(node, "li") + } + } } object GitBucketHtmlSerializer { @@ -171,12 +193,12 @@ object GitBucketHtmlSerializer { } def escapeTaskList(text: String): String = { - Pattern.compile("""^ *- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("task:$1: ") + Pattern.compile("""^( *)- \[([x| ])\] """, Pattern.MULTILINE).matcher(text).replaceAll("$1* task:$2: ") } def convertCheckBox(text: String, hasWritePermission: Boolean): String = { val disabled = if (hasWritePermission) "" else "disabled" - text.replaceAll("task:x:", """") - .replaceAll("task: :", """") + text.replaceAll("task:x:", """") + .replaceAll("task: :", """") } } diff --git a/src/main/webapp/assets/common/css/gitbucket.css b/src/main/webapp/assets/common/css/gitbucket.css index fecaa3605..211277f69 100644 --- a/src/main/webapp/assets/common/css/gitbucket.css +++ b/src/main/webapp/assets/common/css/gitbucket.css @@ -814,6 +814,20 @@ div.attachable div.clickable { background-color: white; } +ul.task-list { + padding-left: 2em; + margin-left: 0; +} + +li.task-list-item { + list-style-type: none; +} + +li.task-list-item input.task-list-item-checkbox { + margin: 0 4px 0.25em -20px; + vertical-align: middle; +} + /****************************************************************************/ /* Pull Request */ /****************************************************************************/ diff --git a/src/test/scala/view/GitBucketHtmlSerializerSpec.scala b/src/test/scala/view/GitBucketHtmlSerializerSpec.scala index 5b49c886a..687db701a 100644 --- a/src/test/scala/view/GitBucketHtmlSerializerSpec.scala +++ b/src/test/scala/view/GitBucketHtmlSerializerSpec.scala @@ -27,28 +27,28 @@ class GitBucketHtmlSerializerSpec extends Specification { } "escapeTaskList" should { - "convert '- [ ] ' to 'task: :'" in { + "convert '- [ ] ' to '* task: :'" in { val before = "- [ ] aaaa" val after = escapeTaskList(before) - after mustEqual "task: : aaaa" + after mustEqual "* task: : aaaa" } - "convert ' - [ ] ' to 'task: :'" in { + "convert ' - [ ] ' to ' * task: :'" in { val before = " - [ ] aaaa" val after = escapeTaskList(before) - after mustEqual "task: : aaaa" + after mustEqual " * task: : aaaa" } "convert only first '- [ ] '" in { val before = " - [ ] aaaa - [ ] bbb" val after = escapeTaskList(before) - after mustEqual "task: : aaaa - [ ] bbb" + after mustEqual " * task: : aaaa - [ ] bbb" } - "convert '- [x] ' to 'task: :'" in { + "convert '- [x] ' to '* task:x:'" in { val before = " - [x] aaaa" val after = escapeTaskList(before) - after mustEqual "task:x: aaaa" + after mustEqual " * task:x: aaaa" } "convert multi lines" in { @@ -60,8 +60,8 @@ tasks val after = escapeTaskList(before) after mustEqual """ tasks -task:x: aaaa -task: : bbb +* task:x: aaaa +* task: : bbb """ } From ba218053f9f7f971c9bcb588a50aa8f1ec5e3aeb Mon Sep 17 00:00:00 2001 From: bati11 Date: Sat, 11 Oct 2014 13:50:32 +0900 Subject: [PATCH 6/6] Modify to correspond to that "issuedetail.scala.html" has been deleted --- src/main/twirl/issues/commentlist.scala.html | 49 ++++++++++++++------ 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/src/main/twirl/issues/commentlist.scala.html b/src/main/twirl/issues/commentlist.scala.html index 1ebf73f93..ff3ff7e2e 100644 --- a/src/main/twirl/issues/commentlist.scala.html +++ b/src/main/twirl/issues/commentlist.scala.html @@ -16,7 +16,7 @@
    - @markdown(issue.content getOrElse "No description provided.", repository, false, true) + @markdown(issue.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
    @@ -143,7 +143,39 @@ $(function(){ return markdown; }; - $('div[id^=commentContent-').on('click', ':checkbox', function(ev){ + var replaceTaskList = function(issueContentHtml, checkboxes) { + var ss = [], + markdown = extractMarkdown(issueContentHtml), + xs = markdown.split(/- \[[x| ]\]/g); + for (var i=0; i