From ce79eaada80a9210c636c6a22a248dd6c76c54d2 Mon Sep 17 00:00:00 2001 From: bati11 Date: Sat, 5 Apr 2014 20:31:30 +0900 Subject: [PATCH 01/34] 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 02/34] 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 03/34] 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 04/34] 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 05/34] 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 06/34] 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 Date: Thu, 9 Oct 2014 22:05:42 +0900 Subject: [PATCH 07/34] (refs #514) Fix problems of renaming repository. --- src/main/scala/app/RepositorySettingsController.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/scala/app/RepositorySettingsController.scala b/src/main/scala/app/RepositorySettingsController.scala index 53c3ef44c..cce95bbca 100644 --- a/src/main/scala/app/RepositorySettingsController.scala +++ b/src/main/scala/app/RepositorySettingsController.scala @@ -94,7 +94,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { } } // Change repository HEAD - using(Git.open(getRepositoryDir(repository.owner, repository.name))) { git => + using(Git.open(getRepositoryDir(repository.owner, form.repositoryName))) { git => git.getRepository.updateRef(Constants.HEAD, true).link(Constants.R_HEADS + defaultBranch) } flash += "info" -> "Repository settings has been updated." From 0f0986afcfea212d7d33345bf6c64e0db8f0783f Mon Sep 17 00:00:00 2001 From: Shintaro Murakami Date: Tue, 7 Oct 2014 22:21:14 +0900 Subject: [PATCH 08/34] (refs #394)Create branch from Web UI --- .../app/RepositoryViewerController.scala | 23 ++++++- src/main/scala/util/JGitUtil.scala | 13 +++- .../twirl/helper/branchcontrol.scala.html | 62 +++++++++++++++++++ src/main/twirl/helper/error.scala.html | 7 +++ src/main/twirl/helper/information.scala.html | 2 +- src/main/twirl/menu.scala.html | 6 +- src/main/twirl/repo/blob.scala.html | 8 +-- src/main/twirl/repo/commits.scala.html | 11 ++-- src/main/twirl/repo/files.scala.html | 14 +++-- .../webapp/assets/common/css/gitbucket.css | 24 +++++++ 10 files changed, 150 insertions(+), 20 deletions(-) create mode 100644 src/main/twirl/helper/branchcontrol.scala.html create mode 100644 src/main/twirl/helper/error.scala.html diff --git a/src/main/scala/app/RepositoryViewerController.scala b/src/main/scala/app/RepositoryViewerController.scala index 42917306d..26a535e5d 100644 --- a/src/main/scala/app/RepositoryViewerController.scala +++ b/src/main/scala/app/RepositoryViewerController.scala @@ -112,7 +112,7 @@ trait RepositoryViewerControllerBase extends ControllerBase { repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository, logs.splitWith{ (commit1, commit2) => view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) - }, page, hasNext) + }, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount)) case Left(_) => NotFound } } @@ -239,6 +239,24 @@ trait RepositoryViewerControllerBase extends ControllerBase { } }) + /** + * Creates a branch. + */ + post("/:owner/:repository/branches")(collaboratorsOnly { repository => + val newBranchName = params.getOrElse("new", halt(400)) + val fromBranchName = params.getOrElse("from", halt(400)) + using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git => + JGitUtil.createBranch(git, fromBranchName, newBranchName) + } match { + case Right(message) => + flash += "info" -> message + redirect(s"/${repository.owner}/${repository.name}/tree/${StringUtil.urlEncode(newBranchName).replace("%2F", "/")}") + case Left(message) => + flash += "error" -> message + redirect(s"/${repository.owner}/${repository.name}/tree/${fromBranchName}") + } + }) + /** * Deletes branch. */ @@ -331,7 +349,8 @@ trait RepositoryViewerControllerBase extends ControllerBase { repo.html.files(revision, repository, if(path == ".") Nil else path.split("/").toList, // current path new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit - files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount)) + files, readme, hasWritePermission(repository.owner, repository.name, context.loginAccount), + flash.get("info"), flash.get("error")) } } getOrElse NotFound } diff --git a/src/main/scala/util/JGitUtil.scala b/src/main/scala/util/JGitUtil.scala index 0e0493daf..5ea43a3a1 100644 --- a/src/main/scala/util/JGitUtil.scala +++ b/src/main/scala/util/JGitUtil.scala @@ -14,7 +14,7 @@ import org.eclipse.jgit.treewalk.filter._ import org.eclipse.jgit.diff.DiffEntry.ChangeType import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException} import java.util.Date -import org.eclipse.jgit.api.errors.NoHeadException +import org.eclipse.jgit.api.errors.{JGitInternalException, InvalidRefNameException, RefAlreadyExistsException, NoHeadException} import service.RepositoryService import org.eclipse.jgit.dircache.DirCacheEntry import org.slf4j.LoggerFactory @@ -507,6 +507,17 @@ object JGitUtil { }.find(_._1 != null) } + def createBranch(git: Git, fromBranch: String, newBranch: String) = { + try { + git.branchCreate().setStartPoint(fromBranch).setName(newBranch).call() + Right("Branch created.") + } catch { + case e: RefAlreadyExistsException => Left("Sorry, that branch already exists.") + // JGitInternalException occurs when new branch name is 'a' and the branch whose name is 'a/*' exists. + case _: InvalidRefNameException | _: JGitInternalException => Left("Sorry, that name is invalid.") + } + } + def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = { val entry = new DirCacheEntry(path) entry.setFileMode(mode) diff --git a/src/main/twirl/helper/branchcontrol.scala.html b/src/main/twirl/helper/branchcontrol.scala.html new file mode 100644 index 000000000..5381688d6 --- /dev/null +++ b/src/main/twirl/helper/branchcontrol.scala.html @@ -0,0 +1,62 @@ +@(branch: String = "", + repository: service.RepositoryService.RepositoryInfo, + hasWritePermission: Boolean)(body: Html)(implicit context: app.Context) +@import context._ +@import view.helpers._ +@helper.html.dropdown( + value = if(branch.length == 40) branch.substring(0, 10) else branch, + prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", + mini = true +) { +
  • Switch branches
  • +
  • + @body + @if(hasWritePermission) { + + } +} + diff --git a/src/main/twirl/helper/error.scala.html b/src/main/twirl/helper/error.scala.html new file mode 100644 index 000000000..00f43e210 --- /dev/null +++ b/src/main/twirl/helper/error.scala.html @@ -0,0 +1,7 @@ +@(error: Option[Any]) +@if(error.isDefined){ +
    + + @error +
    +} \ No newline at end of file diff --git a/src/main/twirl/helper/information.scala.html b/src/main/twirl/helper/information.scala.html index d8bcff593..ff382a203 100644 --- a/src/main/twirl/helper/information.scala.html +++ b/src/main/twirl/helper/information.scala.html @@ -1,7 +1,7 @@ @(info: Option[Any]) @if(info.isDefined){
    - + @info
    } diff --git a/src/main/twirl/menu.scala.html b/src/main/twirl/menu.scala.html index 8076c825c..7ac1dc83b 100644 --- a/src/main/twirl/menu.scala.html +++ b/src/main/twirl/menu.scala.html @@ -1,7 +1,9 @@ @(active: String, repository: service.RepositoryService.RepositoryInfo, id: Option[String] = None, - expand: Boolean = false)(body: Html)(implicit context: app.Context) + expand: Boolean = false, + info: Option[Any] = None, + error: Option[Any] = None)(body: Html)(implicit context: app.Context) @import context._ @import view.helpers._ @@ -31,6 +33,8 @@ }
    + @helper.html.information(info) + @helper.html.error(error) @if(repository.commitCount > 0){
    diff --git a/src/main/twirl/repo/blob.scala.html b/src/main/twirl/repo/blob.scala.html index 2c895ac36..c85fc0a72 100644 --- a/src/main/twirl/repo/blob.scala.html +++ b/src/main/twirl/repo/blob.scala.html @@ -9,10 +9,10 @@ @html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.menu("code", repository){
    - @helper.html.dropdown( - value = if(branch.length == 40) branch.substring(0, 10) else branch, - prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", - mini = true + @helper.html.branchcontrol( + branch, + repository, + hasWritePermission ){ @repository.branchList.map { x =>
  • @helper.html.checkicon(x == branch) @x
  • diff --git a/src/main/twirl/repo/commits.scala.html b/src/main/twirl/repo/commits.scala.html index be2b02f20..4fce02893 100644 --- a/src/main/twirl/repo/commits.scala.html +++ b/src/main/twirl/repo/commits.scala.html @@ -3,16 +3,17 @@ repository: service.RepositoryService.RepositoryInfo, commits: Seq[Seq[util.JGitUtil.CommitInfo]], page: Int, - hasNext: Boolean)(implicit context: app.Context) + hasNext: Boolean, + hasWritePermission: Boolean)(implicit context: app.Context) @import context._ @import view.helpers._ @html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.menu("code", repository){
    - @helper.html.dropdown( - value = if(branch.length == 40) branch.substring(0, 10) else branch, - prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", - mini = true + @helper.html.branchcontrol( + branch, + repository, + hasWritePermission ){ @repository.branchList.map { x =>
  • @helper.html.checkicon(x == branch) @x
  • diff --git a/src/main/twirl/repo/files.scala.html b/src/main/twirl/repo/files.scala.html index 2235d6ecd..d663b570f 100644 --- a/src/main/twirl/repo/files.scala.html +++ b/src/main/twirl/repo/files.scala.html @@ -4,16 +4,18 @@ latestCommit: util.JGitUtil.CommitInfo, files: List[util.JGitUtil.FileInfo], readme: Option[(List[String], String)], - hasWritePermission: Boolean)(implicit context: app.Context) + hasWritePermission: Boolean, + info: Option[Any] = None, + error: Option[Any] = None)(implicit context: app.Context) @import context._ @import view.helpers._ @html.main(s"${repository.owner}/${repository.name}", Some(repository)) { - @html.menu("code", repository, Some(branch), pathList.isEmpty){ + @html.menu("code", repository, Some(branch), pathList.isEmpty, info, error){
    - @helper.html.dropdown( - value = if(branch.length == 40) branch.substring(0, 10) else branch, - prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", - mini = true + @helper.html.branchcontrol( + branch, + repository, + hasWritePermission ){ @repository.branchList.map { x =>
  • @helper.html.checkicon(x == branch) @x
  • diff --git a/src/main/webapp/assets/common/css/gitbucket.css b/src/main/webapp/assets/common/css/gitbucket.css index d84bc0818..551574640 100644 --- a/src/main/webapp/assets/common/css/gitbucket.css +++ b/src/main/webapp/assets/common/css/gitbucket.css @@ -617,6 +617,30 @@ span.simplified-path { color: #888; } +#branch-control-title { + margin: 5px 10px; + font-weight: bold; +} + +#branch-control-close { + background: none; + border: none; + color: #aaa; + font-weight: bold; + line-height: 15px; +} + +#branch-control-input { + border: solid 1px #ccc; + margin: 10px; +} + +.new-branch-name { + font-weight: bold; + font-size: 1.2em; + padding-left: 16px; +} + /****************************************************************************/ /* nav pulls group */ /****************************************************************************/ From e33dd9008b3ac2869659f52ceeff487833d643d5 Mon Sep 17 00:00:00 2001 From: Shintaro Murakami Date: Sat, 18 Oct 2014 23:21:47 +0900 Subject: [PATCH 09/34] (ref #519) Change datetime formats --- src/main/scala/view/helpers.scala | 45 ++++++++++++++++++- .../twirl/account/repositories.scala.html | 2 +- .../twirl/dashboard/issueslist.scala.html | 2 +- src/main/twirl/dashboard/pullslist.scala.html | 2 +- src/main/twirl/helper/activities.scala.html | 6 +-- src/main/twirl/helper/datetimeago.scala.html | 10 +++++ src/main/twirl/issues/commentlist.scala.html | 14 +++--- src/main/twirl/issues/issue.scala.html | 2 +- src/main/twirl/issues/listparts.scala.html | 2 +- .../twirl/issues/milestones/list.scala.html | 2 +- src/main/twirl/pulls/pullreq.scala.html | 2 +- src/main/twirl/repo/blob.scala.html | 2 +- src/main/twirl/repo/branches.scala.html | 2 +- src/main/twirl/repo/commit.scala.html | 4 +- src/main/twirl/repo/commits.scala.html | 4 +- src/main/twirl/repo/files.scala.html | 6 +-- src/main/twirl/repo/tags.scala.html | 2 +- src/main/twirl/search/code.scala.html | 2 +- src/main/twirl/search/issues.scala.html | 2 +- src/main/twirl/wiki/history.scala.html | 2 +- src/main/twirl/wiki/page.scala.html | 2 +- 21 files changed, 85 insertions(+), 32 deletions(-) create mode 100644 src/main/twirl/helper/datetimeago.scala.html diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index 78f658f62..6bb1d0ccb 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -1,5 +1,5 @@ package view -import java.util.{Date, TimeZone} +import java.util.{Locale, Date, TimeZone} import java.text.SimpleDateFormat import play.twirl.api.Html import util.StringUtil @@ -15,6 +15,49 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache */ def datetime(date: Date): String = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(date) + val timeUnits = List( + (1000L, "second"), + (1000L * 60, "minute"), + (1000L * 60 * 60, "hour"), + (1000L * 60 * 60 * 24, "day"), + (1000L * 60 * 60 * 24 * 30, "month"), + (1000L * 60 * 60 * 24 * 365, "year") + ).reverse + + /** + * Format java.util.Date to "x {seconds/minutes/hours/days/months/years} ago" + */ + def datetimeAgo(date: Date): String = { + val duration = new Date().getTime - date.getTime + timeUnits.find(tuple => duration / tuple._1 > 0) match { + case Some((unitValue, unitString)) => + val value = duration / unitValue + s"${value} ${unitString}${if (value > 1) "s" else ""} ago" + case None => "just now" + } + } + + /** + * + * Format java.util.Date to "x {seconds/minutes/hours/days} ago" + * If duration over 1 month, format to "d MMM (yyyy)" + * + */ + def datetimeAgoRecentOnly(date: Date): String = { + val duration = new Date().getTime - date.getTime + val list = timeUnits.map(tuple => (duration / tuple._1, tuple._2)).filter(tuple => tuple._1 > 0) + if (list.isEmpty) + "just now" + else { + list.head match { + case (_, "month") => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}" + case (_, "year") => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}" + case (value, unitString) => s"${value} ${unitString}${if (value > 1) "s" else ""} ago" + } + } + } + + /** * Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'". */ diff --git a/src/main/twirl/account/repositories.scala.html b/src/main/twirl/account/repositories.scala.html index 1ad52dfa8..8a3706b28 100644 --- a/src/main/twirl/account/repositories.scala.html +++ b/src/main/twirl/account/repositories.scala.html @@ -25,7 +25,7 @@ @if(repository.repository.description.isDefined){
    @repository.repository.description
    } -
    Last updated: @datetime(repository.repository.lastActivityDate)
    +
    Updated @helper.html.datetimeago(repository.repository.lastActivityDate)
    } diff --git a/src/main/twirl/dashboard/issueslist.scala.html b/src/main/twirl/dashboard/issueslist.scala.html index 4f27ea8c4..186eb986d 100644 --- a/src/main/twirl/dashboard/issueslist.scala.html +++ b/src/main/twirl/dashboard/issueslist.scala.html @@ -165,7 +165,7 @@ #@issue.issueId
    - Opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)  + Opened by @user(issue.openedUserName, styleClass="username") @helper.html.datetimeago(issue.registeredDate)  @if(commentCount > 0){ @commentCount @plural(commentCount, "comment") } diff --git a/src/main/twirl/dashboard/pullslist.scala.html b/src/main/twirl/dashboard/pullslist.scala.html index 46343b46e..dc90d9ed8 100644 --- a/src/main/twirl/dashboard/pullslist.scala.html +++ b/src/main/twirl/dashboard/pullslist.scala.html @@ -86,7 +86,7 @@ }
    - @avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)  + @avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @helper.html.datetimeago(issue.registeredDate)  @if(commentCount > 0){ @commentCount @plural(commentCount, "comment") } diff --git a/src/main/twirl/helper/activities.scala.html b/src/main/twirl/helper/activities.scala.html index 980eaf53c..9bb3f8df1 100644 --- a/src/main/twirl/helper/activities.scala.html +++ b/src/main/twirl/helper/activities.scala.html @@ -62,7 +62,7 @@ @detailActivity(activity: model.Activity, image: String) = {
    -
    @datetime(activity.activityDate)
    +
    @helper.html.datetimeago(activity.activityDate)
    @avatar(activity.activityUserName, 16) @activityMessage(activity.message) @@ -76,7 +76,7 @@ @customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
    -
    @datetime(activity.activityDate)
    +
    @helper.html.datetimeago(activity.activityDate)
    @avatar(activity.activityUserName, 16) @activityMessage(activity.message) @@ -91,7 +91,7 @@
    @avatar(activity.activityUserName, 16) @activityMessage(activity.message) - @datetime(activity.activityDate) + @helper.html.datetimeago(activity.activityDate)
    } diff --git a/src/main/twirl/helper/datetimeago.scala.html b/src/main/twirl/helper/datetimeago.scala.html new file mode 100644 index 000000000..3b68f34c7 --- /dev/null +++ b/src/main/twirl/helper/datetimeago.scala.html @@ -0,0 +1,10 @@ +@(latestUpdatedDate: java.util.Date, + recentOnly: Boolean = true) +@import view.helpers._ + + @if(recentOnly){ + @datetimeAgoRecentOnly(latestUpdatedDate) + }else{ + @datetimeAgo(latestUpdatedDate) + } + diff --git a/src/main/twirl/issues/commentlist.scala.html b/src/main/twirl/issues/commentlist.scala.html index 979062f17..32532d658 100644 --- a/src/main/twirl/issues/commentlist.scala.html +++ b/src/main/twirl/issues/commentlist.scala.html @@ -8,7 +8,7 @@
    @avatar(issue.openedUserName, 48)
    - @user(issue.openedUserName, styleClass="username strong") commented on @datetime(issue.registeredDate) + @user(issue.openedUserName, styleClass="username strong") commented @helper.html.datetimeago(issue.registeredDate) @if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){ @@ -32,7 +32,7 @@ } else { @if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request } } - on @datetime(comment.registeredDate) + @helper.html.datetimeago(comment.registeredDate) @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" && @@ -70,7 +70,7 @@ } else { @pullreq.map(_.userName):@pullreq.map(_.branch) to @pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch) } - @datetime(comment.registeredDate) + @helper.html.datetimeago(comment.registeredDate)
    } @if(comment.action == "close" || comment.action == "close_comment"){ @@ -78,9 +78,9 @@ Closed @avatar(comment.commentedUserName, 20) @if(issue.isPullRequest){ - @user(comment.commentedUserName, styleClass="username strong") closed the pull request @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") closed the pull request @helper.html.datetimeago(comment.registeredDate) } else { - @user(comment.commentedUserName, styleClass="username strong") closed the issue @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") closed the issue @helper.html.datetimeago(comment.registeredDate) }
    } @@ -88,14 +88,14 @@
    Reopened @avatar(comment.commentedUserName, 20) - @user(comment.commentedUserName, styleClass="username strong") reopened the issue @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") reopened the issue @helper.html.datetimeago(comment.registeredDate)
    } @if(comment.action == "delete_branch"){
    Deleted @avatar(comment.commentedUserName, 20) - @user(comment.commentedUserName, styleClass="username strong") deleted the @pullreq.map(_.requestBranch) branch @datetime(comment.registeredDate) + @user(comment.commentedUserName, styleClass="username strong") deleted the @pullreq.map(_.requestBranch) branch @helper.html.datetimeago(comment.registeredDate)
    } } diff --git a/src/main/twirl/issues/issue.scala.html b/src/main/twirl/issues/issue.scala.html index 3b45ebf3c..3b7f125f6 100644 --- a/src/main/twirl/issues/issue.scala.html +++ b/src/main/twirl/issues/issue.scala.html @@ -28,7 +28,7 @@ Open } - @user(issue.openedUserName, styleClass="username strong") opened this issue on @datetime(issue.registeredDate) - @defining( + @user(issue.openedUserName, styleClass="username strong") opened this issue @helper.html.datetimeago(issue.registeredDate) - @defining( comments.filter( _.action.contains("comment") ).size ){ count => @count @plural(count, "comment") diff --git a/src/main/twirl/issues/listparts.scala.html b/src/main/twirl/issues/listparts.scala.html index d3b5a113f..ab9cbadf7 100644 --- a/src/main/twirl/issues/listparts.scala.html +++ b/src/main/twirl/issues/listparts.scala.html @@ -203,7 +203,7 @@ }
    - #@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate) + #@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username") @milestone.map { milestone => @milestone } diff --git a/src/main/twirl/issues/milestones/list.scala.html b/src/main/twirl/issues/milestones/list.scala.html index 877083205..84a03a403 100644 --- a/src/main/twirl/issues/milestones/list.scala.html +++ b/src/main/twirl/issues/milestones/list.scala.html @@ -34,7 +34,7 @@ @milestone.title
    @if(milestone.closedDate.isDefined){ - Closed @datetime(milestone.closedDate.get) + Closed @helper.html.datetimeago(milestone.closedDate.get) } else { @milestone.dueDate.map { dueDate => @if(isPast(dueDate)){ diff --git a/src/main/twirl/pulls/pullreq.scala.html b/src/main/twirl/pulls/pullreq.scala.html index 35cebb3fd..495379bed 100644 --- a/src/main/twirl/pulls/pullreq.scala.html +++ b/src/main/twirl/pulls/pullreq.scala.html @@ -20,7 +20,7 @@ Merged @user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit") into @pullreq.userName:@pullreq.branch from @pullreq.requestUserName:@pullreq.requestBranch - at @datetime(comment.registeredDate) + @helper.html.datetimeago(comment.registeredDate) }.getOrElse { Closed @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") diff --git a/src/main/twirl/repo/blob.scala.html b/src/main/twirl/repo/blob.scala.html index 2c895ac36..68d3870d6 100644 --- a/src/main/twirl/repo/blob.scala.html +++ b/src/main/twirl/repo/blob.scala.html @@ -34,7 +34,7 @@
    @avatar(latestCommit, 20) @user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong") - @datetime(latestCommit.commitTime) + @helper.html.datetimeago(latestCommit.commitTime) @link(latestCommit.summary, repository)
    diff --git a/src/main/twirl/repo/branches.scala.html b/src/main/twirl/repo/branches.scala.html index 214c37c92..98576841e 100644 --- a/src/main/twirl/repo/branches.scala.html +++ b/src/main/twirl/repo/branches.scala.html @@ -22,7 +22,7 @@ } - @datetime(latestUpdateDate) + @helper.html.datetimeago(latestUpdateDate, false) @if(repository.repository.defaultBranch == branchName){ diff --git a/src/main/twirl/repo/commit.scala.html b/src/main/twirl/repo/commit.scala.html index a02223692..9b7c2f074 100644 --- a/src/main/twirl/repo/commit.scala.html +++ b/src/main/twirl/repo/commit.scala.html @@ -68,13 +68,13 @@
    @avatar(commit, 20) @user(commit.authorName, commit.authorEmailAddress, "username strong") - authored on @datetime(commit.authorTime) + authored @helper.html.datetimeago(commit.authorTime)
    @if(commit.isDifferentFromAuthor) {
    @user(commit.committerName, commit.committerEmailAddress, "username strong") - committed on @datetime(commit.commitTime) + committed @helper.html.datetimeago(commit.commitTime)
    }
    diff --git a/src/main/twirl/repo/commits.scala.html b/src/main/twirl/repo/commits.scala.html index be2b02f20..f0aaf3b9a 100644 --- a/src/main/twirl/repo/commits.scala.html +++ b/src/main/twirl/repo/commits.scala.html @@ -58,11 +58,11 @@ }
    @user(commit.authorName, commit.authorEmailAddress, "username") - authored @datetime(commit.authorTime) + authored @helper.html.datetimeago(commit.authorTime) @if(commit.isDifferentFromAuthor) { @user(commit.committerName, commit.committerEmailAddress, "username") - committed @datetime(commit.authorTime) + committed @helper.html.datetimeago(commit.authorTime) }
    diff --git a/src/main/twirl/repo/files.scala.html b/src/main/twirl/repo/files.scala.html index 2235d6ecd..ab14364a7 100644 --- a/src/main/twirl/repo/files.scala.html +++ b/src/main/twirl/repo/files.scala.html @@ -47,13 +47,13 @@
    @avatar(latestCommit, 20) @user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong") - authored on @datetime(latestCommit.authorTime) + authored @helper.html.datetimeago(latestCommit.authorTime)
    @if(latestCommit.isDifferentFromAuthor) {
    @user(latestCommit.committerName, latestCommit.committerEmailAddress, "username strong") - committed on @datetime(latestCommit.commitTime) + committed @helper.html.datetimeago(latestCommit.commitTime)
    }
    @@ -106,7 +106,7 @@ @link(file.message, repository) [@user(file.author, file.mailAddress)] - @datetime(file.time) + @helper.html.datetimeago(file.time, false) } diff --git a/src/main/twirl/repo/tags.scala.html b/src/main/twirl/repo/tags.scala.html index e5b0fa928..cd611fbb6 100644 --- a/src/main/twirl/repo/tags.scala.html +++ b/src/main/twirl/repo/tags.scala.html @@ -14,7 +14,7 @@ @repository.tags.reverse.map { tag => @tag.name - @datetime(tag.time) + @helper.html.datetimeago(tag.time, false) @tag.id.substring(0, 10) ZIP diff --git a/src/main/twirl/search/code.scala.html b/src/main/twirl/search/code.scala.html index 20b08a611..04d0a1922 100644 --- a/src/main/twirl/search/code.scala.html +++ b/src/main/twirl/search/code.scala.html @@ -16,7 +16,7 @@ @files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
    @file.path
    -
    Latest commit at @datetime(file.lastModified)
    +
    Last commited @helper.html.datetimeago(file.lastModified)
    @Html(file.highlightText)
    } diff --git a/src/main/twirl/search/issues.scala.html b/src/main/twirl/search/issues.scala.html index d381091e2..7a9c17ae9 100644 --- a/src/main/twirl/search/issues.scala.html +++ b/src/main/twirl/search/issues.scala.html @@ -22,7 +22,7 @@ }
    Opened by @issue.openedUserName - at @datetime(issue.registeredDate) + @helper.html.datetimeago(issue.registeredDate) @if(issue.commentCount > 0){   @issue.commentCount @plural(issue.commentCount, "comment") } diff --git a/src/main/twirl/wiki/history.scala.html b/src/main/twirl/wiki/history.scala.html index bd2ba49d5..2052f4ce0 100644 --- a/src/main/twirl/wiki/history.scala.html +++ b/src/main/twirl/wiki/history.scala.html @@ -36,7 +36,7 @@ @avatar(commit, 20) @user(commit.authorName, commit.authorEmailAddress) - @datetime(commit.authorTime): @commit.shortMessage + @helper.html.datetimeago(commit.authorTime): @commit.shortMessage } diff --git a/src/main/twirl/wiki/page.scala.html b/src/main/twirl/wiki/page.scala.html index 8d103d8bf..74dabb2b5 100644 --- a/src/main/twirl/wiki/page.scala.html +++ b/src/main/twirl/wiki/page.scala.html @@ -12,7 +12,7 @@
  • @pageName

    - @page.committer edited this page at @datetime(page.time) + @page.committer edited this page @helper.html.datetimeago(page.time)
  • From 5b875d7c73b58fcc1a8e7285a198995ac637b8f3 Mon Sep 17 00:00:00 2001 From: Shintaro Murakami Date: Sun, 19 Oct 2014 01:22:31 +0900 Subject: [PATCH 10/34] Refactored, sorry. --- src/main/scala/view/helpers.scala | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/main/scala/view/helpers.scala b/src/main/scala/view/helpers.scala index 6bb1d0ccb..a14392978 100644 --- a/src/main/scala/view/helpers.scala +++ b/src/main/scala/view/helpers.scala @@ -45,15 +45,13 @@ object helpers extends AvatarImageProvider with LinkConverter with RequestCache */ def datetimeAgoRecentOnly(date: Date): String = { val duration = new Date().getTime - date.getTime - val list = timeUnits.map(tuple => (duration / tuple._1, tuple._2)).filter(tuple => tuple._1 > 0) - if (list.isEmpty) - "just now" - else { - list.head match { - case (_, "month") => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}" - case (_, "year") => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}" - case (value, unitString) => s"${value} ${unitString}${if (value > 1) "s" else ""} ago" - } + timeUnits.find(tuple => duration / tuple._1 > 0) match { + case Some((_, "month")) => s"on ${new SimpleDateFormat("d MMM", Locale.ENGLISH).format(date)}" + case Some((_, "year")) => s"on ${new SimpleDateFormat("d MMM yyyy", Locale.ENGLISH).format(date)}" + case Some((unitValue, unitString)) => + val value = duration / unitValue + s"${value} ${unitString}${if (value > 1) "s" else ""} ago" + case None => "just now" } } From 13c206d0689dbe9e874b660ffe96bb67205f5e03 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sun, 19 Oct 2014 21:34:12 +0900 Subject: [PATCH 11/34] Applying new Issues UI to dashboard --- src/main/scala/app/DashboardController.scala | 21 +- src/main/twirl/dashboard/issues.scala.html | 52 +--- .../twirl/dashboard/issueslist.scala.html | 277 +++++++----------- 3 files changed, 119 insertions(+), 231 deletions(-) diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala index b470f0ec9..9a3c28f0a 100644 --- a/src/main/scala/app/DashboardController.scala +++ b/src/main/scala/app/DashboardController.scala @@ -12,7 +12,7 @@ trait DashboardControllerBase extends ControllerBase { self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator => get("/dashboard/issues/repos")(usersOnly { - searchIssues("all") + searchIssues("created_by") }) get("/dashboard/issues/assigned")(usersOnly { @@ -54,19 +54,12 @@ trait DashboardControllerBase extends ControllerBase { val page = IssueSearchCondition.page(request) dashboard.html.issues( - dashboard.html.issueslist( - searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), - page, - countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*), - countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*), - condition), - countIssue(condition.copy(assigned = None, author = None), filterUser, false, userRepos: _*), - countIssue(condition.copy(assigned = Some(userName), author = None), filterUser, false, userRepos: _*), - countIssue(condition.copy(assigned = None, author = Some(userName)), filterUser, false, userRepos: _*), - countIssueGroupByRepository(condition, filterUser, false, userRepos: _*), - condition, - filter) - + searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), + page, + countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*), + countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*), + condition, + filter) } private def searchPullRequests(filter: String, repository: Option[String]) = { diff --git a/src/main/twirl/dashboard/issues.scala.html b/src/main/twirl/dashboard/issues.scala.html index f6f9bbb71..4d5af5d0c 100644 --- a/src/main/twirl/dashboard/issues.scala.html +++ b/src/main/twirl/dashboard/issues.scala.html @@ -1,50 +1,14 @@ -@(listparts: play.twirl.api.Html, - allCount: Int, - assignedCount: Int, - createdByCount: Int, - repositories: List[(String, String, Int)], +@(issues: List[service.IssuesService.IssueInfo], + page: Int, + openCount: Int, + closedCount: Int, condition: service.IssuesService.IssueSearchCondition, filter: String)(implicit context: app.Context) @import context._ @import view.helpers._ -@html.main("Your Issues"){ -
    - @dashboard.html.tab("issues") -
    - - @listparts +@html.main("Issues"){ +
    + @dashboard.html.tab("issues") + @issueslist(issues, page, openCount, closedCount, condition, filter)
    -
    } diff --git a/src/main/twirl/dashboard/issueslist.scala.html b/src/main/twirl/dashboard/issueslist.scala.html index 4f27ea8c4..248f68871 100644 --- a/src/main/twirl/dashboard/issueslist.scala.html +++ b/src/main/twirl/dashboard/issueslist.scala.html @@ -3,182 +3,113 @@ openCount: Int, closedCount: Int, condition: service.IssuesService.IssueSearchCondition, - collaborators: List[String] = Nil, - milestones: List[model.Milestone] = Nil, - labels: List[model.Label] = Nil, - repository: Option[service.RepositoryService.RepositoryInfo] = None, - hasWritePermission: Boolean = false)(implicit context: app.Context) + filter: String)(implicit context: app.Context) @import context._ @import view.helpers._ @import service.IssuesService.IssueInfo -
    - @if(condition.labels.nonEmpty || condition.milestoneId.isDefined){ - - Clear milestone and label filters + + + + + + @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => + + + + } +
    + + + + @openCount Open +    + + + @closedCount Closed - } - @if(condition.repo.isDefined){ - - Clear filter on @condition.repo - - } -
    - @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL) -
    - - @helper.html.dropdown( - value = (condition.sort, condition.direction) match { - case ("created" , "desc") => "Newest" - case ("created" , "asc" ) => "Oldest" - case ("comments", "desc") => "Most commented" - case ("comments", "asc" ) => "Least commented" - case ("updated" , "desc") => "Recently updated" - case ("updated" , "asc" ) => "Least recently updated" - }, - prefix = "Sort", - mini = false - ){ -
  • - - @helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest - -
  • -
  • - - @helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest - -
  • -
  • - - @helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented - -
  • -
  • - - @helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented - -
  • -
  • - - @helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated - -
  • -
  • - - @helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated - -
  • - } - - @if(issues.isEmpty){ - - - - } else { - @if(hasWritePermission){ - - - + + - - - } -
    - No issues to show. - @if(condition.labels.nonEmpty || condition.milestoneId.isDefined){ - Clear active filters. - } else { - @if(repository.isDefined){ - Create a new issue. - } - } -
    -
    - -
    - @helper.html.dropdown("Label") { - @labels.map { label => -
  • - - -   - @label.labelName - -
  • - } - } - @helper.html.dropdown("Assignee") { -
  • Clear assignee
  • - @collaborators.map { collaborator => -
  • @avatar(collaborator, 20) @collaborator
  • - } - } - @helper.html.dropdown("Milestone") { -
  • Clear this milestone
  • - @milestones.map { milestone => -
  • - - @milestone.title -
    - @milestone.dueDate.map { dueDate => - @if(isPast(dueDate)){ - Due by @date(dueDate) - } else { - Due by @date(dueDate) - } - }.getOrElse { - No due date - } -
    -
    -
  • - } - } -
    - @if(hasWritePermission){ - - } -
    -
    - @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
    - - +
    + @if(issue.isPullRequest){ + + } else { + + } + @issue.repositoryName ・ + @if(issue.isPullRequest){ + @issue.title + } else { + @issue.title + } + @labels.map { label => + @label.labelName + } + + @issue.assignedUserName.map { userName => + @avatar(userName, 20, tooltip = true) + } + @if(commentCount > 0){ + + @commentCount + + } else { + + @commentCount + + } + +
    + #@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate) + @milestone.map { milestone => + @milestone + } +
    +
    +
    + @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL) +
    From 4c1b8004fcb32cbdde97c2f6a550eeb1d6819cc8 Mon Sep 17 00:00:00 2001 From: Tomofumi Tanaka Date: Wed, 29 Oct 2014 09:15:20 +0900 Subject: [PATCH 12/34] (refs #533)Admin user must not disable self account yourself --- src/main/scala/app/UserManagementController.scala | 12 +++++++++++- src/main/twirl/admin/users/user.scala.html | 3 +++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/scala/app/UserManagementController.scala b/src/main/scala/app/UserManagementController.scala index 612503959..d98d70366 100644 --- a/src/main/scala/app/UserManagementController.scala +++ b/src/main/scala/app/UserManagementController.scala @@ -49,7 +49,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase { "url" -> trim(label("URL" ,optional(text(maxlength(200))))), "fileId" -> trim(label("File ID" ,optional(text()))), "clearImage" -> trim(label("Clear image" ,boolean())), - "removed" -> trim(label("Disable" ,boolean())) + "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName")))) )(EditUserForm.apply) val newGroupForm = mapping( @@ -190,4 +190,14 @@ trait UserManagementControllerBase extends AccountManagementControllerBase { } } + protected def disableByNotYourself(paramName: String): Constraint = new Constraint() { + override def validate(name: String, value: String, messages: Messages): Option[String] = { + params.get(paramName).flatMap { userName => + if(userName == context.loginAccount.get.userName) + Some("You can't disable your account yourself") + else + None + } + } + } } diff --git a/src/main/twirl/admin/users/user.scala.html b/src/main/twirl/admin/users/user.scala.html index fb022c0c2..1f2585777 100644 --- a/src/main/twirl/admin/users/user.scala.html +++ b/src/main/twirl/admin/users/user.scala.html @@ -16,6 +16,9 @@ Disable +
    + +
    } @if(account.map(_.password.nonEmpty).getOrElse(true)){ From e6e3786b4783be14d5c86a04abfb94cdd43de0fa Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sat, 1 Nov 2014 03:05:52 +0900 Subject: [PATCH 13/34] (refs #529)Visibility filter --- src/main/scala/app/DashboardController.scala | 21 +-- src/main/scala/service/IssuesService.scala | 66 +++++---- .../scala/service/PullRequestService.scala | 34 ++--- .../twirl/dashboard/issueslist.scala.html | 13 +- src/main/twirl/dashboard/pulls.scala.html | 44 +----- src/main/twirl/dashboard/pullslist.scala.html | 139 ++++++++++++------ 6 files changed, 177 insertions(+), 140 deletions(-) diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala index 9a3c28f0a..4e0ffb23e 100644 --- a/src/main/scala/app/DashboardController.scala +++ b/src/main/scala/app/DashboardController.scala @@ -74,29 +74,16 @@ trait DashboardControllerBase extends ControllerBase { val userName = context.loginAccount.get.userName val allRepos = getAllRepositories(userName) - val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name) val filterUser = Map(filter -> userName) val page = IssueSearchCondition.page(request) - val counts = countIssueGroupByRepository( - IssueSearchCondition().copy(state = condition.state), filterUser, true, userRepos: _*) - dashboard.html.pulls( - dashboard.html.pullslist( - searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), - page, - countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*), - countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*), - condition, - None, - false), - getAllPullRequestCountGroupByUser(condition.state == "closed", userName), - userRepos.map { case (userName, repoName) => - (userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0)) - }.sortBy(_._3).reverse, + searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), + page, + countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*), + countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*), condition, filter) - } diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala index 5688fc78f..e563b9ccc 100644 --- a/src/main/scala/service/IssuesService.scala +++ b/src/main/scala/service/IssuesService.scala @@ -77,28 +77,29 @@ trait IssuesService { } .toMap } - /** - * Returns list which contains issue count for each repository. - * If the issue does not exist, its repository is not included in the result. - * - * @param condition the search condition - * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. - * @param repos Tuple of the repository owner and the repository name - * @return list which contains issue count for each repository - */ - def countIssueGroupByRepository( - condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, - repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = { - searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) - .groupBy { t => - t.userName -> t.repositoryName - } - .map { case (repo, t) => - (repo._1, repo._2, t.length) - } - .sortBy(_._3 desc) - .list - } + +// /** +// * Returns list which contains issue count for each repository. +// * If the issue does not exist, its repository is not included in the result. +// * +// * @param condition the search condition +// * @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. +// * @param repos Tuple of the repository owner and the repository name +// * @return list which contains issue count for each repository +// */ +// def countIssueGroupByRepository( +// condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, +// repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = { +// searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) +// .groupBy { t => +// t.userName -> t.repositoryName +// } +// .map { case (repo, t) => +// (repo._1, repo._2, t.length) +// } +// .sortBy(_._3 desc) +// .list +// } /** * Returns the search result against issues. @@ -181,7 +182,11 @@ trait IssuesService { (t3.byRepository(t1.userName, t1.repositoryName)) && (t3.labelName inSetBind condition.labels) } map(_.labelId))) - } exists, condition.labels.nonEmpty) + } exists, condition.labels.nonEmpty) && + (Repositories filter { t3 => + (t3.byRepository(t1.userName, t1.repositoryName)) && + (t3.isPrivate === (condition.visibility == Some("private")).bind) + } exists, condition.visibility.nonEmpty) } def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], @@ -343,11 +348,12 @@ object IssuesService { repo: Option[String] = None, state: String = "open", sort: String = "created", - direction: String = "desc"){ + direction: String = "desc", + visibility: Option[String] = None){ def isEmpty: Boolean = { labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty && - state == "open" && sort == "created" && direction == "desc" + state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty } def nonEmpty: Boolean = !isEmpty @@ -364,7 +370,9 @@ object IssuesService { repo.map("for=" + urlEncode(_)), Some("state=" + urlEncode(state)), Some("sort=" + urlEncode(sort)), - Some("direction=" + urlEncode(direction))).flatten.mkString("&") + Some("direction=" + urlEncode(direction)), + visibility.map(x => "visibility=" + urlEncode(x)) + ).flatten.mkString("&") } @@ -378,7 +386,7 @@ object IssuesService { def apply(request: HttpServletRequest): IssueSearchCondition = IssueSearchCondition( param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), - param(request, "milestone").map{ + param(request, "milestone").map { case "none" => None case x => x.toIntOpt }, @@ -387,7 +395,9 @@ object IssuesService { param(request, "for"), param(request, "state", Seq("open", "closed")).getOrElse("open"), param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), - param(request, "direction", Seq("asc", "desc")).getOrElse("desc")) + param(request, "direction", Seq("asc", "desc")).getOrElse("desc"), + param(request, "visibility") + ) def page(request: HttpServletRequest) = try { val i = param(request, "page").getOrElse("1").toInt diff --git a/src/main/scala/service/PullRequestService.scala b/src/main/scala/service/PullRequestService.scala index d5bcb0b2b..9a3239b4c 100644 --- a/src/main/scala/service/PullRequestService.scala +++ b/src/main/scala/service/PullRequestService.scala @@ -36,23 +36,23 @@ trait PullRequestService { self: IssuesService => .list .map { x => PullRequestCount(x._1, x._2) } - def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] = - PullRequests - .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } - .innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) } - .filter { case ((t1, t2), t3) => - (t2.closed === closed.bind) && - ( - (t3.isPrivate === false.bind) || - (t3.userName === userName.bind) || - (Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists) - ) - } - .groupBy { case ((t1, t2), t3) => t2.openedUserName } - .map { case (userName, t) => userName -> t.length } - .sortBy(_._2 desc) - .list - .map { x => PullRequestCount(x._1, x._2) } +// def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] = +// PullRequests +// .innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } +// .innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) } +// .filter { case ((t1, t2), t3) => +// (t2.closed === closed.bind) && +// ( +// (t3.isPrivate === false.bind) || +// (t3.userName === userName.bind) || +// (Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists) +// ) +// } +// .groupBy { case ((t1, t2), t3) => t2.openedUserName } +// .map { case (userName, t) => userName -> t.length } +// .sortBy(_._2 desc) +// .list +// .map { x => PullRequestCount(x._1, x._2) } def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int, originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String, diff --git a/src/main/twirl/dashboard/issueslist.scala.html b/src/main/twirl/dashboard/issueslist.scala.html index 248f68871..9e0bad8f7 100644 --- a/src/main/twirl/dashboard/issueslist.scala.html +++ b/src/main/twirl/dashboard/issueslist.scala.html @@ -29,7 +29,18 @@
    @helper.html.dropdown("Visibility", flat = true){ -
  • TODO
  • +
  • + + @helper.html.checkicon(condition.visibility == Some("private")) + Private repository only + +
  • +
  • + + @helper.html.checkicon(condition.visibility == Some("public")) + Public repository only + +
  • } @helper.html.dropdown("Organization", flat = true){
  • TODO
  • diff --git a/src/main/twirl/dashboard/pulls.scala.html b/src/main/twirl/dashboard/pulls.scala.html index 25caafec6..3b310e101 100644 --- a/src/main/twirl/dashboard/pulls.scala.html +++ b/src/main/twirl/dashboard/pulls.scala.html @@ -1,42 +1,14 @@ -@(listparts: play.twirl.api.Html, - counts: List[service.PullRequestService.PullRequestCount], - repositories: List[(String, String, Int)], +@(issues: List[service.IssuesService.IssueInfo], + page: Int, + openCount: Int, + closedCount: Int, condition: service.IssuesService.IssueSearchCondition, filter: String)(implicit context: app.Context) @import context._ @import view.helpers._ -@html.main("Your Issues"){ -
    - @dashboard.html.tab("pulls") -
    - - @listparts +@html.main("Pull Requests"){ +
    + @dashboard.html.tab("pulls") + @issueslist(issues, page, openCount, closedCount, condition, filter)
    -
    } diff --git a/src/main/twirl/dashboard/pullslist.scala.html b/src/main/twirl/dashboard/pullslist.scala.html index 46343b46e..9cfed59c4 100644 --- a/src/main/twirl/dashboard/pullslist.scala.html +++ b/src/main/twirl/dashboard/pullslist.scala.html @@ -3,20 +3,19 @@ openCount: Int, closedCount: Int, condition: service.IssuesService.IssueSearchCondition, - repository: Option[service.RepositoryService.RepositoryInfo], - hasWritePermission: Boolean)(implicit context: app.Context) + filter: String)(implicit context: app.Context) @import context._ @import view.helpers._ @import service.IssuesService.IssueInfo + +@*
    - @repository.map { repository => - @if(hasWritePermission){ -
    - @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 7, condition.toURL) - New pull request -
    - } - }
    @openCount Open @closedCount Closed @@ -64,38 +63,96 @@ } - - @if(issues.isEmpty){ - - - - } + *@ +
    - No pull requests to show. -
    + + + @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => - - + - +
    + @avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)  + @if(commentCount > 0){ + @commentCount @plural(commentCount, "comment") + } +
    + + } -
    + + + + @openCount Open +    + + + @closedCount Closed + + + +
    - - @issue.title - #@issue.issueId -
    - @issue.content.map { content => - @cut(content, 90) - }.getOrElse { - No description available - } -
    -
    - @avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)  - @if(commentCount > 0){ - @commentCount @plural(commentCount, "comment") +
    + + @issue.title + #@issue.issueId +
    + @issue.content.map { content => + @cut(content, 90) + }.getOrElse { + No description available }
    -
    -
    - @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL) -
    -
    \ No newline at end of file + +
    + @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL) +
    From 2db674bb0310aab96d1784d616883cac303fb5d2 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sun, 2 Nov 2014 13:26:46 +0900 Subject: [PATCH 14/34] (refs #529)Organization filter --- src/main/scala/app/DashboardController.scala | 9 ++- src/main/scala/service/AccountService.scala | 5 ++ src/main/scala/service/IssuesService.scala | 12 ++- src/main/twirl/dashboard/header.scala.html | 74 +++++++++++++++++++ src/main/twirl/dashboard/issues.scala.html | 5 +- .../twirl/dashboard/issueslist.scala.html | 65 +--------------- src/main/twirl/dashboard/pulls.scala.html | 5 +- src/main/twirl/dashboard/pullslist.scala.html | 65 +--------------- 8 files changed, 105 insertions(+), 135 deletions(-) create mode 100644 src/main/twirl/dashboard/header.scala.html diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala index 4e0ffb23e..c8ea7d67d 100644 --- a/src/main/scala/app/DashboardController.scala +++ b/src/main/scala/app/DashboardController.scala @@ -9,7 +9,8 @@ class DashboardController extends DashboardControllerBase with UsersAuthenticator trait DashboardControllerBase extends ControllerBase { - self: IssuesService with PullRequestService with RepositoryService with UsersAuthenticator => + self: IssuesService with PullRequestService with RepositoryService with AccountService + with UsersAuthenticator => get("/dashboard/issues/repos")(usersOnly { searchIssues("created_by") @@ -59,7 +60,8 @@ trait DashboardControllerBase extends ControllerBase { countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*), countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*), condition, - filter) + filter, + getGroupNames(userName)) } private def searchPullRequests(filter: String, repository: Option[String]) = { @@ -83,7 +85,8 @@ trait DashboardControllerBase extends ControllerBase { countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*), countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*), condition, - filter) + filter, + getGroupNames(userName)) } diff --git a/src/main/scala/service/AccountService.scala b/src/main/scala/service/AccountService.scala index b815c5411..c502eb7b4 100644 --- a/src/main/scala/service/AccountService.scala +++ b/src/main/scala/service/AccountService.scala @@ -168,6 +168,11 @@ trait AccountService { Repositories.filter(_.userName === userName.bind).delete } + def getGroupNames(userName: String)(implicit s: Session): List[String] = { + List(userName) ++ + Collaborators.filter(_.collaboratorName === userName.bind).sortBy(_.userName).map(_.userName).list + } + } object AccountService extends AccountService diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala index e563b9ccc..9f5e1e348 100644 --- a/src/main/scala/service/IssuesService.scala +++ b/src/main/scala/service/IssuesService.scala @@ -186,7 +186,8 @@ trait IssuesService { (Repositories filter { t3 => (t3.byRepository(t1.userName, t1.repositoryName)) && (t3.isPrivate === (condition.visibility == Some("private")).bind) - } exists, condition.visibility.nonEmpty) + } exists, condition.visibility.nonEmpty) && + (t1.userName inSetBind condition.groups, condition.groups.nonEmpty) } def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], @@ -349,7 +350,8 @@ object IssuesService { state: String = "open", sort: String = "created", direction: String = "desc", - visibility: Option[String] = None){ + visibility: Option[String] = None, + groups: Set[String] = Set.empty){ def isEmpty: Boolean = { labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty && @@ -371,7 +373,8 @@ object IssuesService { Some("state=" + urlEncode(state)), Some("sort=" + urlEncode(sort)), Some("direction=" + urlEncode(direction)), - visibility.map(x => "visibility=" + urlEncode(x)) + visibility.map(x => "visibility=" + urlEncode(x)), + if(groups.isEmpty) None else Some("groups=" + urlEncode(groups.mkString(","))) ).flatten.mkString("&") } @@ -396,7 +399,8 @@ object IssuesService { param(request, "state", Seq("open", "closed")).getOrElse("open"), param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), param(request, "direction", Seq("asc", "desc")).getOrElse("desc"), - param(request, "visibility") + param(request, "visibility"), + param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty) ) def page(request: HttpServletRequest) = try { diff --git a/src/main/twirl/dashboard/header.scala.html b/src/main/twirl/dashboard/header.scala.html new file mode 100644 index 000000000..01c5bfc2e --- /dev/null +++ b/src/main/twirl/dashboard/header.scala.html @@ -0,0 +1,74 @@ +@(openCount: Int, + closedCount: Int, + condition: service.IssuesService.IssueSearchCondition, + groups: List[String])(implicit context: app.Context) +@import context._ +@import view.helpers._ + + + + @openCount Open +    + + + @closedCount Closed + + + \ No newline at end of file diff --git a/src/main/twirl/dashboard/issues.scala.html b/src/main/twirl/dashboard/issues.scala.html index 4d5af5d0c..09d3a4f6d 100644 --- a/src/main/twirl/dashboard/issues.scala.html +++ b/src/main/twirl/dashboard/issues.scala.html @@ -3,12 +3,13 @@ openCount: Int, closedCount: Int, condition: service.IssuesService.IssueSearchCondition, - filter: String)(implicit context: app.Context) + filter: String, + groups: List[String])(implicit context: app.Context) @import context._ @import view.helpers._ @html.main("Issues"){
    @dashboard.html.tab("issues") - @issueslist(issues, page, openCount, closedCount, condition, filter) + @issueslist(issues, page, openCount, closedCount, condition, filter, groups)
    } diff --git a/src/main/twirl/dashboard/issueslist.scala.html b/src/main/twirl/dashboard/issueslist.scala.html index 9e0bad8f7..b80b24768 100644 --- a/src/main/twirl/dashboard/issueslist.scala.html +++ b/src/main/twirl/dashboard/issueslist.scala.html @@ -3,7 +3,8 @@ openCount: Int, closedCount: Int, condition: service.IssuesService.IssueSearchCondition, - filter: String)(implicit context: app.Context) + filter: String, + groups: List[String])(implicit context: app.Context) @import context._ @import view.helpers._ @import service.IssuesService.IssueInfo @@ -17,67 +18,7 @@ @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => diff --git a/src/main/twirl/dashboard/pulls.scala.html b/src/main/twirl/dashboard/pulls.scala.html index 3b310e101..13ec94b4d 100644 --- a/src/main/twirl/dashboard/pulls.scala.html +++ b/src/main/twirl/dashboard/pulls.scala.html @@ -3,12 +3,13 @@ openCount: Int, closedCount: Int, condition: service.IssuesService.IssueSearchCondition, - filter: String)(implicit context: app.Context) + filter: String, + groups: List[String])(implicit context: app.Context) @import context._ @import view.helpers._ @html.main("Pull Requests"){
    @dashboard.html.tab("pulls") - @issueslist(issues, page, openCount, closedCount, condition, filter) + @issueslist(issues, page, openCount, closedCount, condition, filter, groups)
    } diff --git a/src/main/twirl/dashboard/pullslist.scala.html b/src/main/twirl/dashboard/pullslist.scala.html index 9cfed59c4..eec1c3931 100644 --- a/src/main/twirl/dashboard/pullslist.scala.html +++ b/src/main/twirl/dashboard/pullslist.scala.html @@ -3,7 +3,8 @@ openCount: Int, closedCount: Int, condition: service.IssuesService.IssueSearchCondition, - filter: String)(implicit context: app.Context) + filter: String, + groups: List[String])(implicit context: app.Context) @import context._ @import view.helpers._ @import service.IssuesService.IssueInfo @@ -67,67 +68,7 @@
    - - - - @openCount Open -    - - - @closedCount Closed - - - + @dashboard.html.header(openCount, closedCount, condition, groups)
    @issues.map { case IssueInfo(issue, labels, milestone, commentCount) => From bb3f086aa66ab80f9e4b3432e3ff3567aba50c28 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Mon, 3 Nov 2014 00:31:34 +0900 Subject: [PATCH 15/34] (refs #529)Enhance dashboard header --- src/main/twirl/dashboard/issues.scala.html | 2 +- .../twirl/dashboard/issueslist.scala.html | 6 +- src/main/twirl/dashboard/pulls.scala.html | 2 +- src/main/twirl/dashboard/pullslist.scala.html | 56 +------------------ src/main/twirl/dashboard/tab.scala.html | 51 +++++++++++++---- src/main/twirl/index.scala.html | 2 +- 6 files changed, 48 insertions(+), 71 deletions(-) diff --git a/src/main/twirl/dashboard/issues.scala.html b/src/main/twirl/dashboard/issues.scala.html index 09d3a4f6d..16e3564ab 100644 --- a/src/main/twirl/dashboard/issues.scala.html +++ b/src/main/twirl/dashboard/issues.scala.html @@ -8,8 +8,8 @@ @import context._ @import view.helpers._ @html.main("Issues"){ + @dashboard.html.tab("issues")
    - @dashboard.html.tab("issues") @issueslist(issues, page, openCount, closedCount, condition, filter, groups)
    } diff --git a/src/main/twirl/dashboard/issueslist.scala.html b/src/main/twirl/dashboard/issueslist.scala.html index b80b24768..ae3b1308e 100644 --- a/src/main/twirl/dashboard/issueslist.scala.html +++ b/src/main/twirl/dashboard/issueslist.scala.html @@ -10,10 +10,8 @@ @import service.IssuesService.IssueInfo
    - - - - @openCount Open -    - - - @closedCount Closed - - - + @dashboard.html.header(openCount, closedCount, condition, groups)
    diff --git a/src/main/twirl/dashboard/pulls.scala.html b/src/main/twirl/dashboard/pulls.scala.html index 13ec94b4d..614dc9a0e 100644 --- a/src/main/twirl/dashboard/pulls.scala.html +++ b/src/main/twirl/dashboard/pulls.scala.html @@ -8,8 +8,8 @@ @import context._ @import view.helpers._ @html.main("Pull Requests"){ + @dashboard.html.tab("pulls")
    - @dashboard.html.tab("pulls") @issueslist(issues, page, openCount, closedCount, condition, filter, groups)
    } diff --git a/src/main/twirl/dashboard/pullslist.scala.html b/src/main/twirl/dashboard/pullslist.scala.html index eec1c3931..987764004 100644 --- a/src/main/twirl/dashboard/pullslist.scala.html +++ b/src/main/twirl/dashboard/pullslist.scala.html @@ -10,61 +10,9 @@ @import service.IssuesService.IssueInfo -@* -
    - - @helper.html.dropdown( - value = (condition.sort, condition.direction) match { - case ("created" , "desc") => "Newest" - case ("created" , "asc" ) => "Oldest" - case ("comments", "desc") => "Most commented" - case ("comments", "asc" ) => "Least commented" - case ("updated" , "desc") => "Recently updated" - case ("updated" , "asc" ) => "Least recently updated" - }, - prefix = "Sort", - mini = false - ){ -
  • - - @helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest - -
  • -
  • - - @helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest - -
  • -
  • - - @helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented - -
  • -
  • - - @helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented - -
  • -
  • - - @helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated - -
  • -
  • - - @helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated - -
  • - } - *@
    diff --git a/src/main/twirl/dashboard/tab.scala.html b/src/main/twirl/dashboard/tab.scala.html index 1a4326571..98d07ce8e 100644 --- a/src/main/twirl/dashboard/tab.scala.html +++ b/src/main/twirl/dashboard/tab.scala.html @@ -1,13 +1,44 @@ @(active: String = "")(implicit context: app.Context) @import context._ @import view.helpers._ - +
    +
    + News Feed + @if(loginAccount.isDefined){ + + + Pull Requests + + + + Issues + + } + @* + @if(active == ""){ + activities + } + *@ +
    +
    + \ No newline at end of file diff --git a/src/main/twirl/index.scala.html b/src/main/twirl/index.scala.html index ec8b7ce78..5cbe664c8 100644 --- a/src/main/twirl/index.scala.html +++ b/src/main/twirl/index.scala.html @@ -4,8 +4,8 @@ @import context._ @import view.helpers._ @main("GitBucket"){ + @dashboard.html.tab()
    - @dashboard.html.tab()
    @helper.html.activities(activities) From a9d58698cdeb710bf45bad26456f1d2ad2fccbc0 Mon Sep 17 00:00:00 2001 From: takezoe Date: Mon, 3 Nov 2014 01:40:47 +0900 Subject: [PATCH 16/34] (refs #529)Adjust bottom line --- src/main/twirl/dashboard/tab.scala.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/twirl/dashboard/tab.scala.html b/src/main/twirl/dashboard/tab.scala.html index 98d07ce8e..3ce36059e 100644 --- a/src/main/twirl/dashboard/tab.scala.html +++ b/src/main/twirl/dashboard/tab.scala.html @@ -31,7 +31,7 @@ div.dashboard-nav { div.dashboard-nav a { line-height: 10px; margin-left: 20px; - padding-bottom: 12px; + padding-bottom: 13px; padding-left: 4px; padding-right: 4px; color: #888; From badbe73f4ecf07de052314812ef66e513752258a Mon Sep 17 00:00:00 2001 From: takezoe Date: Mon, 3 Nov 2014 02:30:19 +0900 Subject: [PATCH 17/34] Fix for Firefox --- src/main/twirl/issues/labels/list.scala.html | 2 +- src/main/twirl/issues/listparts.scala.html | 2 +- src/main/twirl/issues/milestones/list.scala.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/twirl/issues/labels/list.scala.html b/src/main/twirl/issues/labels/list.scala.html index f21a88b1b..3493ddd1b 100644 --- a/src/main/twirl/issues/labels/list.scala.html +++ b/src/main/twirl/issues/labels/list.scala.html @@ -7,7 +7,7 @@ @html.main(s"Labels - ${repository.owner}/${repository.name}"){ @html.menu("issues", repository){ @issues.html.tab("labels", hasWritePermission, repository) -   +
    diff --git a/src/main/twirl/issues/listparts.scala.html b/src/main/twirl/issues/listparts.scala.html index ab9cbadf7..1ff944f79 100644 --- a/src/main/twirl/issues/listparts.scala.html +++ b/src/main/twirl/issues/listparts.scala.html @@ -12,6 +12,7 @@ @import context._ @import view.helpers._ @import service.IssuesService.IssueInfo +
    @if(condition.nonEmpty){ } - 
    diff --git a/src/main/twirl/issues/milestones/list.scala.html b/src/main/twirl/issues/milestones/list.scala.html index 84a03a403..b0c498b3a 100644 --- a/src/main/twirl/issues/milestones/list.scala.html +++ b/src/main/twirl/issues/milestones/list.scala.html @@ -7,7 +7,7 @@ @html.main(s"Milestones - ${repository.owner}/${repository.name}"){ @html.menu("issues", repository){ @issues.html.tab("milestones", hasWritePermission, repository) -   +
    From 1c2af36c92db338dc11fc0513420532fa4538b42 Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Mon, 3 Nov 2014 04:35:41 +0900 Subject: [PATCH 18/34] (refs #529)Mentioned filter --- src/main/scala/app/DashboardController.scala | 8 ++++++++ src/main/scala/service/IssuesService.scala | 18 +++++++++++++----- src/main/twirl/dashboard/issueslist.scala.html | 2 +- src/main/twirl/dashboard/pullslist.scala.html | 2 +- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/main/scala/app/DashboardController.scala b/src/main/scala/app/DashboardController.scala index c8ea7d67d..7bf30c1d5 100644 --- a/src/main/scala/app/DashboardController.scala +++ b/src/main/scala/app/DashboardController.scala @@ -24,6 +24,10 @@ trait DashboardControllerBase extends ControllerBase { searchIssues("created_by") }) + get("/dashboard/issues/mentioned")(usersOnly { + searchIssues("mentioned") + }) + get("/dashboard/pulls")(usersOnly { searchPullRequests("created_by", None) }) @@ -32,6 +36,10 @@ trait DashboardControllerBase extends ControllerBase { searchPullRequests("created_by", None) }) + get("/dashboard/pulls/mentioned")(usersOnly { + searchPullRequests("mentioned", None) + }) + get("/dashboard/pulls/public")(usersOnly { searchPullRequests("not_created_by", None) }) diff --git a/src/main/scala/service/IssuesService.scala b/src/main/scala/service/IssuesService.scala index 9f5e1e348..ef39a368f 100644 --- a/src/main/scala/service/IssuesService.scala +++ b/src/main/scala/service/IssuesService.scala @@ -105,7 +105,7 @@ trait IssuesService { * Returns the search result against issues. * * @param condition the search condition - * @param filterUser the filter user name (key is "all", "assigned", "created_by" or "not_created_by", value is the user name) + * @param filterUser the filter user name (key is "all", "assigned", "created_by", "not_created_by" or "mentioned", value is the user name) * @param pullRequest if true then returns only pull requests, false then returns only issues. * @param offset the offset for pagination * @param limit the limit for pagination @@ -175,6 +175,7 @@ trait IssuesService { (t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) && (t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && (t1.pullRequest === pullRequest.bind) && + // Label filter (IssueLabels filter { t2 => (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.labelId in @@ -183,11 +184,18 @@ trait IssuesService { (t3.labelName inSetBind condition.labels) } map(_.labelId))) } exists, condition.labels.nonEmpty) && - (Repositories filter { t3 => - (t3.byRepository(t1.userName, t1.repositoryName)) && - (t3.isPrivate === (condition.visibility == Some("private")).bind) + // Visibility filter + (Repositories filter { t2 => + (t2.byRepository(t1.userName, t1.repositoryName)) && + (t2.isPrivate === (condition.visibility == Some("private")).bind) } exists, condition.visibility.nonEmpty) && - (t1.userName inSetBind condition.groups, condition.groups.nonEmpty) + // Organization (group) filter + (t1.userName inSetBind condition.groups, condition.groups.nonEmpty) && + // Mentioned filter + ((t1.openedUserName === filterUser("mentioned").bind) || t1.assignedUserName === filterUser("mentioned").bind || + (IssueComments filter { t2 => + (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === filterUser("mentioned").bind) + } exists), filterUser.get("mentioned").isDefined) } def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], diff --git a/src/main/twirl/dashboard/issueslist.scala.html b/src/main/twirl/dashboard/issueslist.scala.html index ae3b1308e..79c55177a 100644 --- a/src/main/twirl/dashboard/issueslist.scala.html +++ b/src/main/twirl/dashboard/issueslist.scala.html @@ -11,7 +11,7 @@ diff --git a/src/main/twirl/dashboard/pullslist.scala.html b/src/main/twirl/dashboard/pullslist.scala.html index 987764004..fbddac5ee 100644 --- a/src/main/twirl/dashboard/pullslist.scala.html +++ b/src/main/twirl/dashboard/pullslist.scala.html @@ -11,7 +11,7 @@
    From f59e86f5ca46c2eb1b4bf1afcbe4f1272c53913f Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Mon, 3 Nov 2014 05:25:03 +0900 Subject: [PATCH 19/34] (refs #529)Add icons --- etc/icons.svg | 2592 +++++------------ src/main/twirl/dashboard/tab.scala.html | 19 +- src/main/twirl/index.scala.html | 3 + .../webapp/assets/common/images/menu-feed.png | Bin 0 -> 4404 bytes 4 files changed, 765 insertions(+), 1849 deletions(-) create mode 100644 src/main/webapp/assets/common/images/menu-feed.png diff --git a/etc/icons.svg b/etc/icons.svg index 1d50b97e7..9372a97d2 100644 --- a/etc/icons.svg +++ b/etc/icons.svg @@ -1,1844 +1,754 @@ - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/twirl/dashboard/tab.scala.html b/src/main/twirl/dashboard/tab.scala.html index 3ce36059e..aca044f5e 100644 --- a/src/main/twirl/dashboard/tab.scala.html +++ b/src/main/twirl/dashboard/tab.scala.html @@ -3,22 +3,20 @@ @import view.helpers._
    - News Feed + + + News Feed + @if(loginAccount.isDefined){ - + Pull Requests - + Issues } - @* - @if(active == ""){ - activities - } - *@