Compare commits
141 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
823c52e941 | ||
|
7f42007648 | ||
|
7214ef21d2 | ||
|
18a4492975 | ||
|
99f73b1016 | ||
|
0c1ce6a088 | ||
|
ae6291ab83 | ||
|
617fcf7c99 | ||
|
9df4a74837 | ||
|
966d4251be | ||
|
84b2e9cdcd | ||
|
e29d63c91a | ||
|
805d2b8e79 | ||
|
9983fd1292 | ||
|
1de202e927 | ||
|
4eb9f4a485 | ||
|
a8801e4e41 | ||
|
ee1c84dbf2 | ||
|
e40e1fa6cd | ||
|
055f648ea2 | ||
|
37a399c3a2 | ||
|
bc0b11b60a | ||
|
65a1ca7146 | ||
|
2293030d4e | ||
|
c83fab611e | ||
|
29baf1223c | ||
|
2a60f607ff | ||
|
78f4d26aa0 | ||
|
f59e86f5ca | ||
|
1c2af36c92 | ||
|
badbe73f4e | ||
|
a9d58698cd | ||
|
bb3f086aa6 | ||
|
2db674bb03 | ||
|
4bc4a16a80 | ||
|
d88a105628 | ||
|
15d0c5b506 | ||
|
dbde79d2f2 | ||
|
e6e3786b47 | ||
|
4c1b8004fc | ||
|
ff4052f097 | ||
|
13c206d068 | ||
|
5b875d7c73 | ||
|
e33dd9008b | ||
|
8764910553 | ||
|
4c89c40944 | ||
|
0f0986afcf | ||
|
5d5f1f8bdd | ||
|
03e386b3ce | ||
|
435eac7ae6 | ||
|
bd5df3977d | ||
|
ba218053f9 | ||
|
1fe448a83b | ||
|
26a45d0117 | ||
|
320585a530 | ||
|
ca0f888a99 | ||
|
3b08dc2e41 | ||
|
cc128a49c1 | ||
|
e0148695f2 | ||
|
afe0b1dd71 | ||
|
353852d6da | ||
|
28585d1a3d | ||
|
9d69a48c65 | ||
|
2f95c76634 | ||
|
eac9f0e6ff | ||
|
043fc21e05 | ||
|
5854a75615 | ||
|
7b02946496 | ||
|
70f0ffd4f4 | ||
|
91b82c2652 | ||
|
b1017140aa | ||
|
fc806b8813 | ||
|
836913482b | ||
|
b3df3f44c6 | ||
|
4ffbf89e74 | ||
|
9851c7d93d | ||
|
2201f2b202 | ||
|
c92e71bb7a | ||
|
d271fac350 | ||
|
ce4522fc30 | ||
|
a178c48de6 | ||
|
9d1323a044 | ||
|
43babfed94 | ||
|
6fa7ea30fb | ||
|
d78315695b | ||
|
16021865cb | ||
|
b516be242d | ||
|
0124f7cc3c | ||
|
f3eec35287 | ||
|
fb396a33b0 | ||
|
3370499421 | ||
|
d847e27cf9 | ||
|
9684b158ce | ||
|
8456808a8e | ||
|
9747899a19 | ||
|
099304605e | ||
|
30994d0465 | ||
|
71fdbe7b71 | ||
|
86432c5ffe | ||
|
4dfa1fb0f8 | ||
|
db59a7652f | ||
|
417470a81c | ||
|
cc639da17e | ||
|
f619f4a9bc | ||
|
5dffc2a64e | ||
|
bb63a8d14c | ||
|
c1263cc16d | ||
|
49f2e7d70f | ||
|
f93b535f70 | ||
|
e16d3c823b | ||
|
7a6fdbcf50 | ||
|
46041a3762 | ||
|
20b0553f7f | ||
|
5870cacf44 | ||
|
cb512cd98d | ||
|
90487eb7b7 | ||
|
706fa77de3 | ||
|
26b14ded58 | ||
|
3b1367dd8e | ||
|
e1f310317d | ||
|
937814ec5d | ||
|
b55fc649a6 | ||
|
f4e4506517 | ||
|
287a0b6669 | ||
|
5bddd352af | ||
|
9c6ea8fb9d | ||
|
32e8bf46a7 | ||
|
d61fe1bf84 | ||
|
47dbea947d | ||
|
97c6b0495e | ||
|
a602ece8e9 | ||
|
cf6dca84d8 | ||
|
79432ff8ad | ||
|
b8613431de | ||
|
698eafa562 | ||
|
d33886db89 | ||
|
cde09d3a59 | ||
|
6175eb7c08 | ||
|
ebb9d9329a | ||
|
843722f82e | ||
|
ce79eaada8 |
3
.travis.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
language: scala
|
||||
scala:
|
||||
- 2.11.2
|
26
README.md
@@ -1,4 +1,4 @@
|
||||
GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://buildhive.cloudbees.com/job/takezoe/job/gitbucket/)
|
||||
GitBucket [](https://gitter.im/takezoe/gitbucket) [](https://travis-ci.org/takezoe/gitbucket)
|
||||
=========
|
||||
|
||||
GitBucket is the easily installable Github clone written with Scala.
|
||||
@@ -80,6 +80,30 @@ Run the following commands in `Terminal` to
|
||||
|
||||
Release Notes
|
||||
--------
|
||||
### 2.6 - 24 Nov 2014
|
||||
- Search box at issues and pull requests
|
||||
- Information from administrator
|
||||
- Pull request UI has been updated
|
||||
- Move to TravisCI from Buildhive
|
||||
- Some bug fix and improvements
|
||||
|
||||
### 2.5 - 4 Nov 2014
|
||||
- New Dashboard
|
||||
- Change datetime format
|
||||
- Create branch from Web UI
|
||||
- Task list in Markdown
|
||||
- Some bug fix and improvements
|
||||
|
||||
### 2.4.1 - 6 Oct 2014
|
||||
- Bug fix
|
||||
|
||||
### 2.4 - 6 Oct 2014
|
||||
- New UI is applied to Issues and Pull requests
|
||||
- Side-by-side diff is available
|
||||
- Fix relative path problem in Markdown links and images
|
||||
- Plugin System is disabled in default
|
||||
- Some bug fix and improvements
|
||||
|
||||
### 2.3 - 1 Sep 2014
|
||||
- Scala based plugin system
|
||||
- Embedded Jetty war extraction directory moved to `GITBUCKET_HOME/tmp`
|
||||
|
2451
etc/icons.svg
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 78 KiB |
@@ -42,7 +42,7 @@ object MyBuild extends Build {
|
||||
"org.apache.commons" % "commons-email" % "1.3.1",
|
||||
"org.apache.httpcomponents" % "httpclient" % "4.3",
|
||||
"org.apache.sshd" % "apache-sshd" % "0.11.0",
|
||||
"com.typesafe.slick" %% "slick" % "2.1.0-RC3",
|
||||
"com.typesafe.slick" %% "slick" % "2.1.0",
|
||||
"com.novell.ldap" % "jldap" % "2009-10-07",
|
||||
"org.quartz-scheduler" % "quartz" % "2.2.1",
|
||||
"com.h2database" % "h2" % "1.4.180",
|
||||
@@ -53,7 +53,7 @@ object MyBuild extends Build {
|
||||
"com.typesafe.play" %% "twirl-compiler" % "1.0.2"
|
||||
),
|
||||
EclipseKeys.withSource := true,
|
||||
javacOptions in compile ++= Seq("-target", "6", "-source", "6"),
|
||||
javacOptions in compile ++= Seq("-target", "7", "-source", "7"),
|
||||
testOptions in Test += Tests.Argument(TestFrameworks.Specs2, "junitxml", "console"),
|
||||
packageOptions += Package.MainClass("JettyLauncher")
|
||||
).enablePlugins(SbtTwirl)
|
||||
|
1
sbt.sh
@@ -1 +1,2 @@
|
||||
#!/bin/sh
|
||||
java -Dsbt.log.noformat=true -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m -Xmx512M -Xss2M -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 -jar `dirname $0`/sbt-launch-0.13.5.jar "$@"
|
||||
|
@@ -1,18 +1,33 @@
|
||||
package app
|
||||
|
||||
import service._
|
||||
import util.{UsersAuthenticator, Keys}
|
||||
import util.{StringUtil, UsersAuthenticator, Keys}
|
||||
import util.Implicits._
|
||||
import service.IssuesService.IssueSearchCondition
|
||||
|
||||
class DashboardController extends DashboardControllerBase
|
||||
with IssuesService with PullRequestService with RepositoryService with AccountService
|
||||
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("all")
|
||||
get("/dashboard/issues")(usersOnly {
|
||||
val q = request.getParameter("q")
|
||||
val account = context.loginAccount.get
|
||||
Option(q).map { q =>
|
||||
val condition = IssueSearchCondition(q, Map[String, Int]())
|
||||
q match {
|
||||
case q if(q.contains("is:pr")) => redirect(s"/dashboard/pulls?q=${StringUtil.urlEncode(q)}")
|
||||
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/issues/created_by${condition.toURL}")
|
||||
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/issues/assigned${condition.toURL}")
|
||||
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/issues/mentioned${condition.toURL}")
|
||||
case _ => searchIssues("created_by")
|
||||
}
|
||||
} getOrElse {
|
||||
searchIssues("created_by")
|
||||
}
|
||||
})
|
||||
|
||||
get("/dashboard/issues/assigned")(usersOnly {
|
||||
@@ -23,87 +38,99 @@ trait DashboardControllerBase extends ControllerBase {
|
||||
searchIssues("created_by")
|
||||
})
|
||||
|
||||
get("/dashboard/issues/mentioned")(usersOnly {
|
||||
searchIssues("mentioned")
|
||||
})
|
||||
|
||||
get("/dashboard/pulls")(usersOnly {
|
||||
searchPullRequests("created_by", None)
|
||||
val q = request.getParameter("q")
|
||||
val account = context.loginAccount.get
|
||||
Option(q).map { q =>
|
||||
val condition = IssueSearchCondition(q, Map[String, Int]())
|
||||
q match {
|
||||
case q if(q.contains("is:issue")) => redirect(s"/dashboard/issues?q=${StringUtil.urlEncode(q)}")
|
||||
case q if(q.contains(s"author:${account.userName}")) => redirect(s"/dashboard/pulls/created_by${condition.toURL}")
|
||||
case q if(q.contains(s"assignee:${account.userName}")) => redirect(s"/dashboard/pulls/assigned${condition.toURL}")
|
||||
case q if(q.contains(s"mentions:${account.userName}")) => redirect(s"/dashboard/pulls/mentioned${condition.toURL}")
|
||||
case _ => searchPullRequests("created_by")
|
||||
}
|
||||
} getOrElse {
|
||||
searchPullRequests("created_by")
|
||||
}
|
||||
})
|
||||
|
||||
get("/dashboard/pulls/owned")(usersOnly {
|
||||
searchPullRequests("created_by", None)
|
||||
get("/dashboard/pulls/created_by")(usersOnly {
|
||||
searchPullRequests("created_by")
|
||||
})
|
||||
|
||||
get("/dashboard/pulls/public")(usersOnly {
|
||||
searchPullRequests("not_created_by", None)
|
||||
get("/dashboard/pulls/assigned")(usersOnly {
|
||||
searchPullRequests("assigned")
|
||||
})
|
||||
|
||||
get("/dashboard/pulls/for/:owner/:repository")(usersOnly {
|
||||
searchPullRequests("all", Some(params("owner") + "/" + params("repository")))
|
||||
get("/dashboard/pulls/mentioned")(usersOnly {
|
||||
searchPullRequests("mentioned")
|
||||
})
|
||||
|
||||
private def getOrCreateCondition(key: String, filter: String, userName: String) = {
|
||||
val condition = session.putAndGet(key, if(request.hasQueryString){
|
||||
val q = request.getParameter("q")
|
||||
if(q == null){
|
||||
IssueSearchCondition(request)
|
||||
} else {
|
||||
IssueSearchCondition(q, Map[String, Int]())
|
||||
}
|
||||
} else session.getAs[IssueSearchCondition](key).getOrElse(IssueSearchCondition()))
|
||||
|
||||
filter match {
|
||||
case "assigned" => condition.copy(assigned = Some(userName), author = None , mentioned = None)
|
||||
case "mentioned" => condition.copy(assigned = None , author = None , mentioned = Some(userName))
|
||||
case _ => condition.copy(assigned = None , author = Some(userName), mentioned = None)
|
||||
}
|
||||
}
|
||||
|
||||
private def searchIssues(filter: String) = {
|
||||
import IssuesService._
|
||||
|
||||
// condition
|
||||
val condition = session.putAndGet(Keys.Session.DashboardIssues,
|
||||
if(request.hasQueryString) IssueSearchCondition(request)
|
||||
else session.getAs[IssueSearchCondition](Keys.Session.DashboardIssues).getOrElse(IssueSearchCondition())
|
||||
)
|
||||
|
||||
val userName = context.loginAccount.get.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 userName = context.loginAccount.get.userName
|
||||
val condition = getOrCreateCondition(Keys.Session.DashboardIssues, filter, userName)
|
||||
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
|
||||
dashboard.html.issues(
|
||||
issues.html.listparts(
|
||||
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, Map.empty, false, userRepos: _*),
|
||||
countIssue(condition, Map("assigned" -> userName), false, userRepos: _*),
|
||||
countIssue(condition, Map("created_by" -> userName), false, userRepos: _*),
|
||||
countIssueGroupByRepository(condition, filterUser, false, userRepos: _*),
|
||||
condition,
|
||||
filter)
|
||||
|
||||
searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
|
||||
page,
|
||||
countIssue(condition.copy(state = "open" ), false, userRepos: _*),
|
||||
countIssue(condition.copy(state = "closed"), false, userRepos: _*),
|
||||
filter match {
|
||||
case "assigned" => condition.copy(assigned = Some(userName))
|
||||
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||
case _ => condition.copy(author = Some(userName))
|
||||
},
|
||||
filter,
|
||||
getGroupNames(userName))
|
||||
}
|
||||
|
||||
private def searchPullRequests(filter: String, repository: Option[String]) = {
|
||||
private def searchPullRequests(filter: String) = {
|
||||
import IssuesService._
|
||||
import PullRequestService._
|
||||
|
||||
// condition
|
||||
val condition = session.putAndGet(Keys.Session.DashboardPulls, {
|
||||
if(request.hasQueryString) IssueSearchCondition(request)
|
||||
else session.getAs[IssueSearchCondition](Keys.Session.DashboardPulls).getOrElse(IssueSearchCondition())
|
||||
}.copy(repo = repository))
|
||||
|
||||
val userName = context.loginAccount.get.userName
|
||||
val allRepos = getAllRepositories()
|
||||
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), Map.empty, true, userRepos: _*)
|
||||
val userName = context.loginAccount.get.userName
|
||||
val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
|
||||
val allRepos = getAllRepositories(userName)
|
||||
val page = IssueSearchCondition.page(request)
|
||||
|
||||
dashboard.html.pulls(
|
||||
pulls.html.listparts(
|
||||
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),
|
||||
getPullRequestCountGroupByUser(condition.state == "closed", None, None),
|
||||
userRepos.map { case (userName, repoName) =>
|
||||
(userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0))
|
||||
}.sortBy(_._3).reverse,
|
||||
condition,
|
||||
filter)
|
||||
|
||||
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
|
||||
page,
|
||||
countIssue(condition.copy(state = "open" ), true, allRepos: _*),
|
||||
countIssue(condition.copy(state = "closed"), true, allRepos: _*),
|
||||
filter match {
|
||||
case "assigned" => condition.copy(assigned = Some(userName))
|
||||
case "mentioned" => condition.copy(mentioned = Some(userName))
|
||||
case _ => condition.copy(author = Some(userName))
|
||||
},
|
||||
filter,
|
||||
getGroupNames(userName))
|
||||
}
|
||||
|
||||
|
||||
|
@@ -9,7 +9,6 @@ import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
import org.scalatra.Ok
|
||||
import model.Issue
|
||||
import plugin.PluginSystem
|
||||
|
||||
class IssuesController extends IssuesControllerBase
|
||||
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
|
||||
@@ -21,7 +20,6 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
|
||||
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])
|
||||
|
||||
@@ -33,10 +31,12 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
"labelNames" -> trim(optional(text()))
|
||||
)(IssueCreateForm.apply)
|
||||
|
||||
val issueTitleEditForm = mapping(
|
||||
"title" -> trim(label("Title", text(required)))
|
||||
)(x => x)
|
||||
val issueEditForm = mapping(
|
||||
"title" -> trim(label("Title", text(required))),
|
||||
"content" -> trim(optional(text()))
|
||||
)(IssueEditForm.apply)
|
||||
"content" -> trim(optional(text()))
|
||||
)(x => x)
|
||||
|
||||
val commentForm = mapping(
|
||||
"issueId" -> label("Issue Id", number()),
|
||||
@@ -48,16 +48,13 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
"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")(referrersOnly { repository =>
|
||||
val q = request.getParameter("q")
|
||||
if(Option(q).exists(_.contains("is:pr"))){
|
||||
redirect(s"/${repository.owner}/${repository.name}/pulls?q=" + StringUtil.urlEncode(q))
|
||||
} else {
|
||||
searchIssues(repository)
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
|
||||
@@ -126,14 +123,29 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) =>
|
||||
ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, 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)
|
||||
updateIssue(owner, name, issue.issueId, title, issue.content)
|
||||
// extract references and create refer comment
|
||||
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse(""))
|
||||
createReferComment(owner, name, issue.copy(title = title), title)
|
||||
|
||||
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||
} else Unauthorized
|
||||
} getOrElse NotFound
|
||||
}
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, 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, issue.title, content)
|
||||
// extract references and create refer comment
|
||||
createReferComment(owner, name, issue, content.getOrElse(""))
|
||||
|
||||
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
|
||||
} else Unauthorized
|
||||
@@ -181,13 +193,13 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
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)
|
||||
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)
|
||||
repository, false, true, true, isEditable(x.userName, x.repositoryName, x.openedUserName))
|
||||
))
|
||||
}
|
||||
} else Unauthorized
|
||||
@@ -204,7 +216,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, isEditable(x.userName, x.repositoryName, x.commentedUserName))
|
||||
))
|
||||
}
|
||||
} else Unauthorized
|
||||
@@ -235,15 +247,17 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
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)
|
||||
issues.milestones.html.progress(openCount + closeCount, closeCount)
|
||||
} getOrElse NotFound
|
||||
} getOrElse Ok()
|
||||
})
|
||||
|
||||
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
|
||||
defining(params.get("value")){ action =>
|
||||
executeBatch(repository) {
|
||||
handleComment(_, None, repository)( _ => action)
|
||||
action match {
|
||||
case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
|
||||
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
|
||||
case _ => // TODO BadRequest
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -293,7 +307,10 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
|
||||
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
|
||||
params("checked").split(',') map(_.toInt) foreach execute
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||
params("from") match {
|
||||
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||
case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls")
|
||||
}
|
||||
}
|
||||
|
||||
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
|
||||
@@ -319,15 +336,15 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
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 _))
|
||||
}
|
||||
case "close" if(!issue.closed) => true ->
|
||||
(Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
|
||||
case "reopen" if(issue.closed) => false ->
|
||||
(Some("reopen") -> Some(recordReopenIssueActivity _))
|
||||
}
|
||||
.map { case (closed, t) =>
|
||||
updateClosed(owner, name, issueId, closed)
|
||||
t
|
||||
}
|
||||
updateClosed(owner, name, issueId, closed)
|
||||
t
|
||||
}
|
||||
.getOrElse(None -> None)
|
||||
|
||||
val commentId = content
|
||||
@@ -337,7 +354,7 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
case (content, action) => createComment(owner, name, userName, issueId, content, action)
|
||||
}
|
||||
|
||||
// record activity
|
||||
// record comment activity if comment is entered
|
||||
content foreach {
|
||||
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
|
||||
(owner, name, userName, issueId, _)
|
||||
@@ -370,32 +387,33 @@ trait IssuesControllerBase extends ControllerBase {
|
||||
}
|
||||
}
|
||||
|
||||
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = {
|
||||
private def searchIssues(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())
|
||||
if(request.hasQueryString){
|
||||
val q = request.getParameter("q")
|
||||
if(q == null){
|
||||
IssueSearchCondition(request)
|
||||
} else {
|
||||
IssueSearchCondition(q, getMilestones(owner, repoName).map(x => (x.title, x.milestoneId)).toMap)
|
||||
}
|
||||
} else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||
)
|
||||
|
||||
issues.html.list(
|
||||
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
|
||||
"issues",
|
||||
searchIssue(condition, 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),
|
||||
countIssue(condition.copy(state = "open" ), false, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), false, owner -> repoName),
|
||||
condition,
|
||||
filter,
|
||||
repository,
|
||||
hasWritePermission(owner, repoName, context.loginAccount))
|
||||
}
|
||||
|
@@ -2,51 +2,67 @@ package app
|
||||
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import service._
|
||||
import util.CollaboratorsAuthenticator
|
||||
import util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
|
||||
import util.Implicits._
|
||||
import org.scalatra.i18n.Messages
|
||||
import org.scalatra.Ok
|
||||
|
||||
class LabelsController extends LabelsControllerBase
|
||||
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator
|
||||
with LabelsService with IssuesService with RepositoryService with AccountService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator
|
||||
|
||||
trait LabelsControllerBase extends ControllerBase {
|
||||
self: LabelsService with RepositoryService with CollaboratorsAuthenticator =>
|
||||
self: LabelsService with IssuesService with RepositoryService
|
||||
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
|
||||
|
||||
case class LabelForm(labelName: String, color: String)
|
||||
|
||||
val newForm = mapping(
|
||||
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
|
||||
"newColor" -> trim(label("Color", text(required, color)))
|
||||
val labelForm = mapping(
|
||||
"labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
|
||||
"labelColor" -> trim(label("Color", text(required, color)))
|
||||
)(LabelForm.apply)
|
||||
|
||||
val editForm = mapping(
|
||||
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
|
||||
"editColor" -> trim(label("Color", text(required, color)))
|
||||
)(LabelForm.apply)
|
||||
|
||||
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) =>
|
||||
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues")
|
||||
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
|
||||
issues.labels.html.list(
|
||||
getLabels(repository.owner, repository.name),
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
repository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository =>
|
||||
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
|
||||
ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
|
||||
issues.labels.html.edit(None, repository)
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/issues/label/:labelId/edit")(collaboratorsOnly { repository =>
|
||||
ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) =>
|
||||
val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
|
||||
issues.labels.html.label(
|
||||
getLabel(repository.owner, repository.name, labelId).get,
|
||||
// TODO futility
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
repository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
|
||||
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
|
||||
issues.labels.html.edit(Some(label), repository)
|
||||
} getOrElse NotFound()
|
||||
})
|
||||
|
||||
ajaxPost("/:owner/:repository/issues/label/:labelId/edit", editForm)(collaboratorsOnly { (form, repository) =>
|
||||
ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) =>
|
||||
updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
|
||||
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
|
||||
issues.labels.html.label(
|
||||
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
|
||||
// TODO futility
|
||||
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
|
||||
repository,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
})
|
||||
|
||||
ajaxGet("/:owner/:repository/issues/label/:labelId/delete")(collaboratorsOnly { repository =>
|
||||
ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
|
||||
deleteLabel(repository.owner, repository.name, params("labelId").toInt)
|
||||
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository)
|
||||
Ok()
|
||||
})
|
||||
|
||||
/**
|
||||
|
@@ -1,6 +1,6 @@
|
||||
package app
|
||||
|
||||
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys}
|
||||
import util._
|
||||
import util.Directory._
|
||||
import util.Implicits._
|
||||
import util.ControlUtil._
|
||||
@@ -18,6 +18,9 @@ import org.slf4j.LoggerFactory
|
||||
import org.eclipse.jgit.merge.MergeStrategy
|
||||
import org.eclipse.jgit.errors.NoMergeBaseException
|
||||
import service.WebHookService.WebHookPayload
|
||||
import util.JGitUtil.DiffInfo
|
||||
import scala.Some
|
||||
import util.JGitUtil.CommitInfo
|
||||
|
||||
class PullRequestsController extends PullRequestsControllerBase
|
||||
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
|
||||
@@ -59,11 +62,12 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
case class MergeForm(message: String)
|
||||
|
||||
get("/:owner/:repository/pulls")(referrersOnly { repository =>
|
||||
searchPullRequests(None, repository)
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
|
||||
searchPullRequests(Some(params("userName")), repository)
|
||||
val q = request.getParameter("q")
|
||||
if(Option(q).exists(_.contains("is:issue"))){
|
||||
redirect(s"/${repository.owner}/${repository.name}/issues?q=" + StringUtil.urlEncode(q))
|
||||
} else {
|
||||
searchPullRequests(None, repository)
|
||||
}
|
||||
})
|
||||
|
||||
get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
|
||||
@@ -453,7 +457,6 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
|
||||
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
|
||||
defining(repository.owner, repository.name){ case (owner, repoName) =>
|
||||
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
|
||||
val page = IssueSearchCondition.page(request)
|
||||
val sessionKey = Keys.Session.Pulls(owner, repoName)
|
||||
|
||||
@@ -463,14 +466,15 @@ trait PullRequestsControllerBase extends ControllerBase {
|
||||
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
|
||||
)
|
||||
|
||||
pulls.html.list(
|
||||
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||
getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)),
|
||||
userName,
|
||||
issues.html.list(
|
||||
"pulls",
|
||||
searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
|
||||
page,
|
||||
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName),
|
||||
countIssue(condition, Map.empty, true, owner -> repoName),
|
||||
(getCollaborators(owner, repoName) :+ owner).sorted,
|
||||
getMilestones(owner, repoName),
|
||||
getLabels(owner, repoName),
|
||||
countIssue(condition.copy(state = "open" ), true, owner -> repoName),
|
||||
countIssue(condition.copy(state = "closed"), true, owner -> repoName),
|
||||
condition,
|
||||
repository,
|
||||
hasWritePermission(owner, repoName, context.loginAccount))
|
||||
|
@@ -2,10 +2,8 @@ package app
|
||||
|
||||
import service._
|
||||
import util.Directory._
|
||||
import util.ControlUtil._
|
||||
import util.Implicits._
|
||||
import util.{LockUtil, UsersAuthenticator, OwnerAuthenticator}
|
||||
import util.JGitUtil.CommitInfo
|
||||
import jp.sf.amateras.scalatra.forms._
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.scalatra.i18n.Messages
|
||||
@@ -13,6 +11,7 @@ import service.WebHookService.WebHookPayload
|
||||
import util.JGitUtil.CommitInfo
|
||||
import util.ControlUtil._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.lib.Constants
|
||||
|
||||
class RepositorySettingsController extends RepositorySettingsControllerBase
|
||||
with RepositoryService with AccountService with WebHookService
|
||||
@@ -71,11 +70,12 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Save the repository options.
|
||||
*/
|
||||
post("/:owner/:repository/settings/options", optionsForm)(ownerOnly { (form, repository) =>
|
||||
val defaultBranch = if(repository.branchList.isEmpty) "master" else form.defaultBranch
|
||||
saveRepositoryOptions(
|
||||
repository.owner,
|
||||
repository.name,
|
||||
form.description,
|
||||
if(repository.branchList.isEmpty) "master" else form.defaultBranch,
|
||||
defaultBranch,
|
||||
repository.repository.parentUserName.map { _ =>
|
||||
repository.repository.isPrivate
|
||||
} getOrElse form.isPrivate
|
||||
@@ -93,6 +93,10 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
FileUtils.moveDirectory(dir, getWikiRepositoryDir(repository.owner, form.repositoryName))
|
||||
}
|
||||
}
|
||||
// Change repository HEAD
|
||||
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."
|
||||
redirect(s"/${repository.owner}/${form.repositoryName}/settings/options")
|
||||
})
|
||||
@@ -131,7 +135,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
* Display the web hook page.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks")(ownerOnly { repository =>
|
||||
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), repository, flash.get("info"))
|
||||
settings.html.hooks(getWebHookURLs(repository.owner, repository.name), flash.get("url"), repository, flash.get("info"))
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -153,7 +157,7 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
/**
|
||||
* Send the test request to registered web hook URLs.
|
||||
*/
|
||||
get("/:owner/:repository/settings/hooks/test")(ownerOnly { repository =>
|
||||
post("/:owner/:repository/settings/hooks/test", webHookForm)(ownerOnly { (form, repository) =>
|
||||
using(Git.open(getRepositoryDir(repository.owner, repository.name))){ git =>
|
||||
import scala.collection.JavaConverters._
|
||||
val commits = git.log
|
||||
@@ -161,15 +165,13 @@ trait RepositorySettingsControllerBase extends ControllerBase {
|
||||
.setMaxCount(3)
|
||||
.call.iterator.asScala.map(new CommitInfo(_))
|
||||
|
||||
getWebHookURLs(repository.owner, repository.name) match {
|
||||
case webHookURLs if(webHookURLs.nonEmpty) =>
|
||||
for(ownerAccount <- getAccountByUserName(repository.owner)){
|
||||
callWebHook(repository.owner, repository.name, webHookURLs,
|
||||
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount))
|
||||
}
|
||||
case _ =>
|
||||
getAccountByUserName(repository.owner).foreach { ownerAccount =>
|
||||
callWebHook(repository.owner, repository.name,
|
||||
List(model.WebHook(repository.owner, repository.name, form.url)),
|
||||
WebHookPayload(git, ownerAccount, "refs/heads/" + repository.repository.defaultBranch, repository, commits.toList, ownerAccount)
|
||||
)
|
||||
}
|
||||
|
||||
flash += "url" -> form.url
|
||||
flash += "info" -> "Test payload deployed!"
|
||||
}
|
||||
redirect(s"/${repository.owner}/${repository.name}/settings/hooks")
|
||||
|
@@ -77,7 +77,9 @@ 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,
|
||||
hasWritePermission(repository.owner, repository.name, context.loginAccount))
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -112,7 +114,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 +241,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 +351,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
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
|
||||
private val form = mapping(
|
||||
"baseUrl" -> trim(label("Base URL", optional(text()))),
|
||||
"information" -> trim(label("Information", optional(text()))),
|
||||
"allowAccountRegistration" -> trim(label("Account registration", boolean())),
|
||||
"gravatar" -> trim(label("Gravatar", boolean())),
|
||||
"notification" -> trim(label("Notification", boolean())),
|
||||
@@ -85,41 +86,55 @@ trait SystemSettingsControllerBase extends ControllerBase {
|
||||
})
|
||||
|
||||
get("/admin/plugins")(adminOnly {
|
||||
val installedPlugins = plugin.PluginSystem.plugins
|
||||
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
|
||||
admin.plugins.html.installed(installedPlugins, updatablePlugins)
|
||||
if(enablePluginSystem){
|
||||
val installedPlugins = plugin.PluginSystem.plugins
|
||||
val updatablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "updatable")
|
||||
admin.plugins.html.installed(installedPlugins, updatablePlugins)
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
post("/admin/plugins/_update", pluginForm)(adminOnly { form =>
|
||||
deletePlugins(form.pluginIds)
|
||||
installPlugins(form.pluginIds)
|
||||
redirect("/admin/plugins")
|
||||
if(enablePluginSystem){
|
||||
deletePlugins(form.pluginIds)
|
||||
installPlugins(form.pluginIds)
|
||||
redirect("/admin/plugins")
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
post("/admin/plugins/_delete", pluginForm)(adminOnly { form =>
|
||||
deletePlugins(form.pluginIds)
|
||||
redirect("/admin/plugins")
|
||||
if(enablePluginSystem){
|
||||
deletePlugins(form.pluginIds)
|
||||
redirect("/admin/plugins")
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
get("/admin/plugins/available")(adminOnly {
|
||||
val installedPlugins = plugin.PluginSystem.plugins
|
||||
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
|
||||
admin.plugins.html.available(availablePlugins)
|
||||
if(enablePluginSystem){
|
||||
val installedPlugins = plugin.PluginSystem.plugins
|
||||
val availablePlugins = getAvailablePlugins(installedPlugins).filter(_.status == "available")
|
||||
admin.plugins.html.available(availablePlugins)
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
post("/admin/plugins/_install", pluginForm)(adminOnly { form =>
|
||||
installPlugins(form.pluginIds)
|
||||
redirect("/admin/plugins")
|
||||
if(enablePluginSystem){
|
||||
installPlugins(form.pluginIds)
|
||||
redirect("/admin/plugins")
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
get("/admin/plugins/console")(adminOnly {
|
||||
admin.plugins.html.console()
|
||||
if(enablePluginSystem){
|
||||
admin.plugins.html.console()
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
post("/admin/plugins/console")(adminOnly {
|
||||
val script = request.getParameter("script")
|
||||
val result = plugin.ScalaPlugin.eval(script)
|
||||
Ok()
|
||||
if(enablePluginSystem){
|
||||
val script = request.getParameter("script")
|
||||
val result = plugin.ScalaPlugin.eval(script)
|
||||
Ok()
|
||||
} else NotFound
|
||||
})
|
||||
|
||||
// TODO Move these methods to PluginSystem or Service?
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -31,7 +31,7 @@ case class Label(
|
||||
if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){
|
||||
"000000"
|
||||
} else {
|
||||
"FFFFFF"
|
||||
"ffffff"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,6 +7,7 @@ package object plugin {
|
||||
|
||||
case class Redirect(path: String)
|
||||
case class Fragment(html: Html)
|
||||
case class RawData(contentType: String, content: Array[Byte])
|
||||
|
||||
object db {
|
||||
// TODO labelled place holder support
|
||||
|
@@ -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
|
||||
|
@@ -43,14 +43,13 @@ trait IssuesService {
|
||||
* Returns the count of the search result against issues.
|
||||
*
|
||||
* @param condition the search condition
|
||||
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
||||
* @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
|
||||
* @param repos Tuple of the repository owner and the repository name
|
||||
* @return the count of the search result
|
||||
*/
|
||||
def countIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
||||
def countIssue(condition: IssueSearchCondition, onlyPullRequest: Boolean,
|
||||
repos: (String, String)*)(implicit s: Session): Int =
|
||||
Query(searchIssueQuery(repos, condition, filterUser, onlyPullRequest).length).first
|
||||
Query(searchIssueQuery(repos, condition, onlyPullRequest).length).first
|
||||
|
||||
/**
|
||||
* Returns the Map which contains issue count for each labels.
|
||||
@@ -58,13 +57,12 @@ trait IssuesService {
|
||||
* @param owner the repository owner
|
||||
* @param repository the repository name
|
||||
* @param condition the search condition
|
||||
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
||||
* @return the Map which contains issue count for each labels (key is label name, value is issue count)
|
||||
*/
|
||||
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
|
||||
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = {
|
||||
|
||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), filterUser, false)
|
||||
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false)
|
||||
.innerJoin(IssueLabels).on { (t1, t2) =>
|
||||
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
|
||||
}
|
||||
@@ -79,47 +77,22 @@ 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 filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
|
||||
* @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.
|
||||
*
|
||||
* @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 onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
|
||||
* @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
|
||||
* @param repos Tuple of the repository owner and the repository name
|
||||
* @return the search result (list of tuples which contain issue, labels and comment count)
|
||||
*/
|
||||
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
|
||||
offset: Int, limit: Int, repos: (String, String)*)
|
||||
(implicit s: Session): List[(Issue, List[Label], Int)] = {
|
||||
def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*)
|
||||
(implicit s: Session): List[IssueInfo] = {
|
||||
|
||||
// get issues and comment count and labels
|
||||
searchIssueQuery(repos, condition, filterUser, onlyPullRequest)
|
||||
searchIssueQuery(repos, condition, pullRequest)
|
||||
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
|
||||
.sortBy { case (t1, t2) =>
|
||||
(condition.sort match {
|
||||
@@ -136,21 +109,23 @@ trait IssuesService {
|
||||
.drop(offset).take(limit)
|
||||
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
|
||||
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
|
||||
.map { case (((t1, t2), t3), t4) =>
|
||||
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?)
|
||||
.leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
|
||||
.map { case ((((t1, t2), t3), t4), t5) =>
|
||||
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?)
|
||||
}
|
||||
.list
|
||||
.splitWith { (c1, c2) =>
|
||||
c1._1.userName == c2._1.userName &&
|
||||
c1._1.userName == c2._1.userName &&
|
||||
c1._1.repositoryName == c2._1.repositoryName &&
|
||||
c1._1.issueId == c2._1.issueId
|
||||
c1._1.issueId == c2._1.issueId
|
||||
}
|
||||
.map { issues => issues.head match {
|
||||
case (issue, commentCount, _,_,_) =>
|
||||
(issue,
|
||||
case (issue, commentCount, _, _, _, milestone) =>
|
||||
IssueInfo(issue,
|
||||
issues.flatMap { t => t._3.map (
|
||||
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
|
||||
)} toList,
|
||||
milestone,
|
||||
commentCount)
|
||||
}} toList
|
||||
}
|
||||
@@ -158,21 +133,18 @@ trait IssuesService {
|
||||
/**
|
||||
* Assembles query for conditional issue searching.
|
||||
*/
|
||||
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition,
|
||||
filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) =
|
||||
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, pullRequest: Boolean)(implicit s: Session) =
|
||||
Issues filter { t1 =>
|
||||
condition.repo
|
||||
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
|
||||
.getOrElse (repos)
|
||||
.map { case (owner, repository) => t1.byRepository(owner, repository) }
|
||||
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
|
||||
repos
|
||||
.map { case (owner, repository) => t1.byRepository(owner, repository) }
|
||||
.foldLeft[Column[Boolean]](false) ( _ || _ ) &&
|
||||
(t1.closed === (condition.state == "closed").bind) &&
|
||||
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
|
||||
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) &&
|
||||
(t1.assignedUserName === filterUser("assigned").bind, filterUser.get("assigned").isDefined) &&
|
||||
(t1.openedUserName === filterUser("created_by").bind, filterUser.get("created_by").isDefined) &&
|
||||
(t1.openedUserName =!= filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) &&
|
||||
(t1.pullRequest === true.bind, onlyPullRequest) &&
|
||||
(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
|
||||
@@ -180,7 +152,19 @@ trait IssuesService {
|
||||
(t3.byRepository(t1.userName, t1.repositoryName)) &&
|
||||
(t3.labelName inSetBind condition.labels)
|
||||
} map(_.labelId)))
|
||||
} exists, condition.labels.nonEmpty)
|
||||
} exists, condition.labels.nonEmpty) &&
|
||||
// Visibility filter
|
||||
(Repositories filter { t2 =>
|
||||
(t2.byRepository(t1.userName, t1.repositoryName)) &&
|
||||
(t2.isPrivate === (condition.visibility == Some("private")).bind)
|
||||
} exists, condition.visibility.nonEmpty) &&
|
||||
// Organization (group) filter
|
||||
(t1.userName inSetBind condition.groups, condition.groups.nonEmpty) &&
|
||||
// Mentioned filter
|
||||
((t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind ||
|
||||
(IssueComments filter { t2 =>
|
||||
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
|
||||
} exists), condition.mentioned.isDefined)
|
||||
}
|
||||
|
||||
def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
|
||||
@@ -337,22 +321,64 @@ object IssuesService {
|
||||
case class IssueSearchCondition(
|
||||
labels: Set[String] = Set.empty,
|
||||
milestoneId: Option[Option[Int]] = None,
|
||||
repo: Option[String] = None,
|
||||
author: Option[String] = None,
|
||||
assigned: Option[String] = None,
|
||||
mentioned: Option[String] = None,
|
||||
state: String = "open",
|
||||
sort: String = "created",
|
||||
direction: String = "desc"){
|
||||
direction: String = "desc",
|
||||
visibility: Option[String] = None,
|
||||
groups: Set[String] = Set.empty){
|
||||
|
||||
def isEmpty: Boolean = {
|
||||
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty &&
|
||||
state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty
|
||||
}
|
||||
|
||||
def nonEmpty: Boolean = !isEmpty
|
||||
|
||||
def toFilterString: String = (
|
||||
List(
|
||||
Some(s"is:${state}"),
|
||||
author.map(author => s"author:${author}"),
|
||||
assigned.map(assignee => s"assignee:${assignee}"),
|
||||
mentioned.map(mentioned => s"mentions:${mentioned}")
|
||||
).flatten ++
|
||||
labels.map(label => s"label:${label}") ++
|
||||
List(
|
||||
milestoneId.map { _ match {
|
||||
case Some(x) => s"milestone:${milestoneId}"
|
||||
case None => "no:milestone"
|
||||
}},
|
||||
(sort, direction) match {
|
||||
case ("created" , "desc") => None
|
||||
case ("created" , "asc" ) => Some("sort:created-asc")
|
||||
case ("comments", "desc") => Some("sort:comments-desc")
|
||||
case ("comments", "asc" ) => Some("sort:comments-asc")
|
||||
case ("updated" , "desc") => Some("sort:updated-desc")
|
||||
case ("updated" , "asc" ) => Some("sort:updated-asc")
|
||||
},
|
||||
visibility.map(visibility => s"visibility:${visibility}")
|
||||
).flatten ++
|
||||
groups.map(group => s"group:${group}")
|
||||
).mkString(" ")
|
||||
|
||||
def toURL: String =
|
||||
"?" + List(
|
||||
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
|
||||
milestoneId.map { id => "milestone=" + (id match {
|
||||
case Some(x) => x.toString
|
||||
case None => "none"
|
||||
})},
|
||||
repo.map("for=" + urlEncode(_)),
|
||||
milestoneId.map { _ match {
|
||||
case Some(x) => "milestone=" + x
|
||||
case None => "milestone=none"
|
||||
}},
|
||||
author .map(x => "author=" + urlEncode(x)),
|
||||
assigned .map(x => "assigned=" + urlEncode(x)),
|
||||
mentioned.map(x => "mentioned=" + urlEncode(x)),
|
||||
Some("state=" + urlEncode(state)),
|
||||
Some("sort=" + urlEncode(sort)),
|
||||
Some("direction=" + urlEncode(direction))).flatten.mkString("&")
|
||||
Some("direction=" + urlEncode(direction)),
|
||||
visibility.map(x => "visibility=" + urlEncode(x)),
|
||||
if(groups.isEmpty) None else Some("groups=" + urlEncode(groups.mkString(",")))
|
||||
).flatten.mkString("&")
|
||||
|
||||
}
|
||||
|
||||
@@ -363,17 +389,63 @@ object IssuesService {
|
||||
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores IssueSearchCondition instance from filter query.
|
||||
*/
|
||||
def apply(filter: String, milestones: Map[String, Int]): IssueSearchCondition = {
|
||||
val conditions = filter.split("[ \t]+").map { x =>
|
||||
val dim = x.split(":")
|
||||
dim(0) -> dim(1)
|
||||
}.groupBy(_._1).map { case (key, values) =>
|
||||
key -> values.map(_._2).toSeq
|
||||
}
|
||||
|
||||
val (sort, direction) = conditions.get("sort").flatMap(_.headOption).getOrElse("created-desc") match {
|
||||
case "created-asc" => ("created" , "asc" )
|
||||
case "comments-desc" => ("comments", "desc")
|
||||
case "comments-asc" => ("comments", "asc" )
|
||||
case "updated-desc" => ("comments", "desc")
|
||||
case "updated-asc" => ("comments", "asc" )
|
||||
case _ => ("created" , "desc")
|
||||
}
|
||||
|
||||
IssueSearchCondition(
|
||||
conditions.get("label").map(_.toSet).getOrElse(Set.empty),
|
||||
conditions.get("milestone").flatMap(_.headOption) match {
|
||||
case None => None
|
||||
case Some("none") => Some(None)
|
||||
case Some(x) => milestones.get(x).map(x => Some(x))
|
||||
},
|
||||
conditions.get("author").flatMap(_.headOption),
|
||||
conditions.get("assignee").flatMap(_.headOption),
|
||||
conditions.get("mentions").flatMap(_.headOption),
|
||||
conditions.get("is").getOrElse(Seq.empty).filter(x => x == "open" || x == "closed").headOption.getOrElse("open"),
|
||||
sort,
|
||||
direction,
|
||||
conditions.get("visibility").flatMap(_.headOption),
|
||||
conditions.get("group").map(_.toSet).getOrElse(Set.empty)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores IssueSearchCondition instance from request parameters.
|
||||
*/
|
||||
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
|
||||
},
|
||||
param(request, "for"),
|
||||
param(request, "author"),
|
||||
param(request, "assigned"),
|
||||
param(request, "mentioned"),
|
||||
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"),
|
||||
param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty)
|
||||
)
|
||||
|
||||
def page(request: HttpServletRequest) = try {
|
||||
val i = param(request, "page").getOrElse("1").toInt
|
||||
@@ -383,4 +455,6 @@ object IssuesService {
|
||||
}
|
||||
}
|
||||
|
||||
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int)
|
||||
|
||||
}
|
||||
|
@@ -12,8 +12,8 @@ trait LabelsService {
|
||||
def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] =
|
||||
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
|
||||
|
||||
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Unit =
|
||||
Labels insert Label(
|
||||
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int =
|
||||
Labels returning Labels.map(_.labelId) += Label(
|
||||
userName = owner,
|
||||
repositoryName = repository,
|
||||
labelName = labelName,
|
||||
|
@@ -36,6 +36,24 @@ 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 createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
|
||||
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,
|
||||
commitIdFrom: String, commitIdTo: String)(implicit s: Session): Unit =
|
||||
|
@@ -54,7 +54,6 @@ trait RepositoryService { self: AccountService =>
|
||||
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val activities = Activities .filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
|
||||
|
||||
Repositories.filter { t =>
|
||||
@@ -69,11 +68,18 @@ trait RepositoryService { self: AccountService =>
|
||||
t.requestRepositoryName === oldRepositoryName.bind
|
||||
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName)
|
||||
|
||||
// Updates activity fk before deleting repository because activity is sorted by activityId
|
||||
// and it can't be changed by deleting-and-inserting record.
|
||||
Activities.filter(_.byRepository(oldUserName, oldRepositoryName)).list.foreach { activity =>
|
||||
Activities.filter(_.activityId === activity.activityId.bind)
|
||||
.map(x => (x.userName, x.repositoryName)).update(newUserName, newRepositoryName)
|
||||
}
|
||||
|
||||
deleteRepository(oldUserName, oldRepositoryName)
|
||||
|
||||
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
|
||||
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
|
||||
|
||||
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
|
||||
Issues.insertAll(issues.map { x => x.copy(
|
||||
@@ -88,7 +94,7 @@ trait RepositoryService { self: AccountService =>
|
||||
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
IssueLabels .insertAll(issueLabels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
Activities .insertAll(activities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
|
||||
|
||||
if(account.isGroupAccount){
|
||||
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
|
||||
} else {
|
||||
@@ -96,12 +102,9 @@ trait RepositoryService { self: AccountService =>
|
||||
}
|
||||
|
||||
// Update activity messages
|
||||
val updateActivities = Activities.filter { t =>
|
||||
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") ||
|
||||
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
|
||||
}.map { t => t.activityId -> t.message }.list
|
||||
|
||||
updateActivities.foreach { case (activityId, message) =>
|
||||
Activities.filter { t =>
|
||||
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || (t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
|
||||
}.map { t => t.activityId -> t.message }.list.foreach { case (activityId, message) =>
|
||||
Activities.filter(_.activityId === activityId.bind).map(_.message).update(
|
||||
message
|
||||
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")
|
||||
@@ -166,8 +169,19 @@ trait RepositoryService { self: AccountService =>
|
||||
}
|
||||
}
|
||||
|
||||
def getAllRepositories()(implicit s: Session): List[(String, String)] = {
|
||||
Repositories.sortBy(_.lastActivityDate desc).map{ t =>
|
||||
/**
|
||||
* Returns the repositories without private repository that user does not have access right.
|
||||
* Include public repository, private own repository and private but collaborator repository.
|
||||
*
|
||||
* @param userName the user name of collaborator
|
||||
* @return the repository infomation list
|
||||
*/
|
||||
def getAllRepositories(userName: String)(implicit s: Session): List[(String, String)] = {
|
||||
Repositories.filter { t1 =>
|
||||
(t1.isPrivate === false.bind) ||
|
||||
(t1.userName === userName.bind) ||
|
||||
(Collaborators.filter { t2 => t2.byRepository(t1.userName, t1.repositoryName) && (t2.collaboratorName === userName.bind)} exists)
|
||||
}.sortBy(_.lastActivityDate desc).map{ t =>
|
||||
(t.userName, t.repositoryName)
|
||||
}.list
|
||||
}
|
||||
|
@@ -12,6 +12,7 @@ trait SystemSettingsService {
|
||||
def saveSystemSettings(settings: SystemSettings): Unit = {
|
||||
defining(new java.util.Properties()){ props =>
|
||||
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", "")))
|
||||
settings.information.foreach(x => props.setProperty(Information, x))
|
||||
props.setProperty(AllowAccountRegistration, settings.allowAccountRegistration.toString)
|
||||
props.setProperty(Gravatar, settings.gravatar.toString)
|
||||
props.setProperty(Notification, settings.notification.toString)
|
||||
@@ -60,6 +61,7 @@ trait SystemSettingsService {
|
||||
}
|
||||
SystemSettings(
|
||||
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
|
||||
getOptionValue[String](props, Information, None),
|
||||
getValue(props, AllowAccountRegistration, false),
|
||||
getValue(props, Gravatar, true),
|
||||
getValue(props, Notification, false),
|
||||
@@ -105,6 +107,7 @@ object SystemSettingsService {
|
||||
|
||||
case class SystemSettings(
|
||||
baseUrl: Option[String],
|
||||
information: Option[String],
|
||||
allowAccountRegistration: Boolean,
|
||||
gravatar: Boolean,
|
||||
notification: Boolean,
|
||||
@@ -147,6 +150,7 @@ object SystemSettingsService {
|
||||
val DefaultLdapPort = 389
|
||||
|
||||
private val BaseURL = "base_url"
|
||||
private val Information = "information"
|
||||
private val AllowAccountRegistration = "allow_account_registration"
|
||||
private val Gravatar = "gravatar"
|
||||
private val Notification = "notification"
|
||||
@@ -191,4 +195,7 @@ object SystemSettingsService {
|
||||
else value
|
||||
}
|
||||
|
||||
// TODO temporary flag
|
||||
val enablePluginSystem = Option(System.getProperty("enable.plugin")).getOrElse("false").toBoolean
|
||||
|
||||
}
|
||||
|
@@ -11,6 +11,7 @@ import util.ControlUtil._
|
||||
import org.eclipse.jgit.api.Git
|
||||
import util.Directory
|
||||
import plugin.PluginUpdateJob
|
||||
import service.SystemSettingsService
|
||||
|
||||
object AutoUpdate {
|
||||
|
||||
@@ -52,6 +53,9 @@ object AutoUpdate {
|
||||
* The history of versions. A head of this sequence is the current BitBucket version.
|
||||
*/
|
||||
val versions = Seq(
|
||||
new Version(2, 6),
|
||||
new Version(2, 5),
|
||||
new Version(2, 4),
|
||||
new Version(2, 3) {
|
||||
override def update(conn: Connection): Unit = {
|
||||
super.update(conn)
|
||||
@@ -168,18 +172,15 @@ object AutoUpdate {
|
||||
*/
|
||||
class AutoUpdateListener extends ServletContextListener {
|
||||
import org.quartz.impl.StdSchedulerFactory
|
||||
import org.quartz.JobBuilder._
|
||||
import org.quartz.TriggerBuilder._
|
||||
import org.quartz.SimpleScheduleBuilder._
|
||||
import AutoUpdate._
|
||||
|
||||
private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
|
||||
private val scheduler = StdSchedulerFactory.getDefaultScheduler
|
||||
|
||||
override def contextInitialized(event: ServletContextEvent): Unit = {
|
||||
val datadir = event.getServletContext.getInitParameter("gitbucket.home")
|
||||
if(datadir != null){
|
||||
System.setProperty("gitbucket.home", datadir)
|
||||
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
|
||||
if(dataDir != null){
|
||||
System.setProperty("gitbucket.home", dataDir)
|
||||
}
|
||||
org.h2.Driver.load()
|
||||
|
||||
@@ -210,21 +211,23 @@ class AutoUpdateListener extends ServletContextListener {
|
||||
logger.debug("End schema update")
|
||||
}
|
||||
|
||||
getDatabase(context).withSession { implicit session =>
|
||||
logger.debug("Starting plugin system...")
|
||||
try {
|
||||
plugin.PluginSystem.init()
|
||||
if(SystemSettingsService.enablePluginSystem){
|
||||
getDatabase(context).withSession { implicit session =>
|
||||
logger.debug("Starting plugin system...")
|
||||
try {
|
||||
plugin.PluginSystem.init()
|
||||
|
||||
scheduler.start()
|
||||
PluginUpdateJob.schedule(scheduler)
|
||||
logger.debug("PluginUpdateJob is started.")
|
||||
scheduler.start()
|
||||
PluginUpdateJob.schedule(scheduler)
|
||||
logger.debug("PluginUpdateJob is started.")
|
||||
|
||||
logger.debug("Plugin system is initialized.")
|
||||
} catch {
|
||||
case ex: Throwable => {
|
||||
logger.error("Failed to initialize plugin system", ex)
|
||||
ex.printStackTrace()
|
||||
throw ex
|
||||
logger.debug("Plugin system is initialized.")
|
||||
} catch {
|
||||
case ex: Throwable => {
|
||||
logger.error("Failed to initialize plugin system", ex)
|
||||
ex.printStackTrace()
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -134,8 +134,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
|
||||
|
||||
// Retrieve all issue count in the repository
|
||||
val issueCount =
|
||||
countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) +
|
||||
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository)
|
||||
countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
|
||||
countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
|
||||
|
||||
// Extract new commit and apply issue comment
|
||||
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch
|
||||
|
@@ -7,7 +7,7 @@ import play.twirl.api.Html
|
||||
import service.{AccountService, RepositoryService, SystemSettingsService}
|
||||
import model.{Account, Session}
|
||||
import util.{JGitUtil, Keys}
|
||||
import plugin.{Fragment, PluginConnectionHolder, Redirect}
|
||||
import plugin.{RawData, Fragment, PluginConnectionHolder, Redirect}
|
||||
import service.RepositoryService.RepositoryInfo
|
||||
import plugin.Security._
|
||||
|
||||
@@ -21,7 +21,7 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
|
||||
(req, res) match {
|
||||
case (request: HttpServletRequest, response: HttpServletResponse) => {
|
||||
Database(req.getServletContext) withTransaction { implicit session =>
|
||||
val path = req.asInstanceOf[HttpServletRequest].getRequestURI
|
||||
val path = request.getRequestURI.substring(request.getServletContext.getContextPath.length)
|
||||
if(!processGlobalAction(path, request, response) && !processRepositoryAction(path, request, response)){
|
||||
chain.doFilter(req, res)
|
||||
}
|
||||
@@ -46,13 +46,7 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
|
||||
} finally {
|
||||
PluginConnectionHolder.threadLocal.remove()
|
||||
}
|
||||
result match {
|
||||
case x: String => renderGlobalHtml(request, response, context, x)
|
||||
case x: Html => renderGlobalHtml(request, response, context, x.toString)
|
||||
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
|
||||
case x: Redirect => response.sendRedirect(x.path)
|
||||
case x: AnyRef => renderJson(request, response, x)
|
||||
}
|
||||
processActionResult(result, request, response, context)
|
||||
} else {
|
||||
// TODO NotFound or Error?
|
||||
}
|
||||
@@ -81,13 +75,7 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
|
||||
} finally {
|
||||
PluginConnectionHolder.threadLocal.remove()
|
||||
}
|
||||
result match {
|
||||
case x: String => renderRepositoryHtml(request, response, context, repository, x)
|
||||
case x: Html => renderGlobalHtml(request, response, context, x.toString)
|
||||
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
|
||||
case x: Redirect => response.sendRedirect(x.path)
|
||||
case x: AnyRef => renderJson(request, response, x)
|
||||
}
|
||||
processActionResult(result, request, response, context)
|
||||
} else {
|
||||
// TODO NotFound or Error?
|
||||
}
|
||||
@@ -97,6 +85,24 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
|
||||
} else false
|
||||
}
|
||||
|
||||
private def processActionResult(result: Any, request: HttpServletRequest, response: HttpServletResponse,
|
||||
context: app.Context): Unit = {
|
||||
result match {
|
||||
case null|None => renderError(request, response, context, 404)
|
||||
case x: String => renderGlobalHtml(request, response, context, x)
|
||||
case Some(x: String) => renderGlobalHtml(request, response, context, x)
|
||||
case x: Html => renderGlobalHtml(request, response, context, x.toString)
|
||||
case Some(x: Html) => renderGlobalHtml(request, response, context, x.toString)
|
||||
case x: Fragment => renderFragmentHtml(request, response, context, x.html.toString)
|
||||
case Some(x: Fragment) => renderFragmentHtml(request, response, context, x.html.toString)
|
||||
case x: RawData => renderRawData(request, response, context, x)
|
||||
case Some(x: RawData) => renderRawData(request, response, context, x)
|
||||
case x: Redirect => response.sendRedirect(x.path)
|
||||
case Some(x: Redirect) => response.sendRedirect(x.path)
|
||||
case x: AnyRef => renderJson(request, response, x)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authentication for global action
|
||||
*/
|
||||
@@ -145,6 +151,10 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
|
||||
}
|
||||
}
|
||||
|
||||
private def renderError(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, error: Int): Unit = {
|
||||
response.sendError(error)
|
||||
}
|
||||
|
||||
private def renderGlobalHtml(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, body: String): Unit = {
|
||||
response.setContentType("text/html; charset=UTF-8")
|
||||
val html = _root_.html.main("GitBucket", None)(Html(body))(context)
|
||||
@@ -162,6 +172,11 @@ class PluginActionInvokeFilter extends Filter with SystemSettingsService with Re
|
||||
IOUtils.write(body.getBytes("UTF-8"), response.getOutputStream)
|
||||
}
|
||||
|
||||
private def renderRawData(request: HttpServletRequest, response: HttpServletResponse, context: app.Context, rawData: RawData): Unit = {
|
||||
response.setContentType(rawData.contentType)
|
||||
IOUtils.write(rawData.content, response.getOutputStream)
|
||||
}
|
||||
|
||||
private def renderJson(request: HttpServletRequest, response: HttpServletResponse, obj: AnyRef): Unit = {
|
||||
import org.json4s._
|
||||
import org.json4s.jackson.Serialization
|
||||
|
@@ -4,6 +4,7 @@ import org.eclipse.jgit.api.Git
|
||||
import util.Directory._
|
||||
import util.StringUtil._
|
||||
import util.ControlUtil._
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.JavaConverters._
|
||||
import org.eclipse.jgit.lib._
|
||||
import org.eclipse.jgit.revwalk._
|
||||
@@ -13,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
|
||||
@@ -190,38 +191,23 @@ object JGitUtil {
|
||||
* @return HTML of the file list
|
||||
*/
|
||||
def getFileList(git: Git, revision: String, path: String = "."): List[FileInfo] = {
|
||||
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
|
||||
var list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
|
||||
|
||||
using(new RevWalk(git.getRepository)){ revWalk =>
|
||||
val objectId = git.getRepository.resolve(revision)
|
||||
val revCommit = revWalk.parseCommit(objectId)
|
||||
|
||||
using(new TreeWalk(git.getRepository)){ treeWalk =>
|
||||
val treeWalk = if (path == ".") {
|
||||
val treeWalk = new TreeWalk(git.getRepository)
|
||||
treeWalk.addTree(revCommit.getTree)
|
||||
if(path != "."){
|
||||
treeWalk.setRecursive(true)
|
||||
treeWalk.setFilter(new TreeFilter(){
|
||||
treeWalk
|
||||
} else {
|
||||
val treeWalk = TreeWalk.forPath(git.getRepository, path, revCommit.getTree)
|
||||
treeWalk.enterSubtree()
|
||||
treeWalk
|
||||
}
|
||||
|
||||
var stopRecursive = false
|
||||
|
||||
def include(walker: TreeWalk): Boolean = {
|
||||
val targetPath = walker.getPathString
|
||||
if((path + "/").startsWith(targetPath)){
|
||||
true
|
||||
} else if(targetPath.startsWith(path + "/") && targetPath.substring(path.length + 1).indexOf('/') < 0){
|
||||
stopRecursive = true
|
||||
treeWalk.setRecursive(false)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
def shouldBeRecursive(): Boolean = !stopRecursive
|
||||
|
||||
override def clone: TreeFilter = return this
|
||||
})
|
||||
}
|
||||
using(treeWalk) { treeWalk =>
|
||||
while (treeWalk.next()) {
|
||||
// submodule
|
||||
val linkUrl = if(treeWalk.getFileMode(0) == FileMode.GITLINK){
|
||||
@@ -230,6 +216,31 @@ object JGitUtil {
|
||||
|
||||
list.append((treeWalk.getObjectId(0), treeWalk.getFileMode(0), treeWalk.getPathString, treeWalk.getNameString, linkUrl))
|
||||
}
|
||||
|
||||
list = list.map(tuple =>
|
||||
if (tuple._2 != FileMode.TREE)
|
||||
tuple
|
||||
else
|
||||
simplifyPath(tuple)
|
||||
)
|
||||
|
||||
@tailrec
|
||||
def simplifyPath(tuple: (ObjectId, FileMode, String, String, Option[String])): (ObjectId, FileMode, String, String, Option[String]) = {
|
||||
val list = new scala.collection.mutable.ListBuffer[(ObjectId, FileMode, String, String, Option[String])]
|
||||
using(new TreeWalk(git.getRepository)) { walk =>
|
||||
walk.addTree(tuple._1)
|
||||
while (walk.next() && list.size < 2) {
|
||||
val linkUrl = if (walk.getFileMode(0) == FileMode.GITLINK) {
|
||||
getSubmodules(git, revCommit.getTree).find(_.path == walk.getPathString).map(_.url)
|
||||
} else None
|
||||
list.append((walk.getObjectId(0), walk.getFileMode(0), tuple._3 + "/" + walk.getPathString, tuple._4 + "/" + walk.getNameString, linkUrl))
|
||||
}
|
||||
}
|
||||
if (list.size != 1 || list.exists(_._2 != FileMode.TREE))
|
||||
tuple
|
||||
else
|
||||
simplifyPath(list(0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -496,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)
|
||||
|
@@ -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}
|
||||
|
||||
@@ -18,17 +19,23 @@ 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, hasWritePermission: 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, hasWritePermission).toHtml(rootNode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,15 +89,18 @@ class GitBucketHtmlSerializer(
|
||||
markdown: String,
|
||||
repository: service.RepositoryService.RepositoryInfo,
|
||||
enableWikiLink: Boolean,
|
||||
enableRefsLink: Boolean
|
||||
enableRefsLink: 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 LinkConverter with RequestCache {
|
||||
|
||||
override protected def printImageTag(imageNode: SuperNode, url: String): Unit =
|
||||
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url)).print("\">")
|
||||
.print("<img src=\"").print(fixUrl(url)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>")
|
||||
override protected def printImageTag(imageNode: SuperNode, url: String): Unit = {
|
||||
printer.print("<a target=\"_blank\" href=\"").print(fixUrl(url, true)).print("\">")
|
||||
.print("<img src=\"").print(fixUrl(url, true)).print("\" alt=\"").printEncoded(printChildrenToString(imageNode)).print("\"/></a>")
|
||||
}
|
||||
|
||||
override protected def printLink(rendering: LinkRenderer.Rendering): Unit = {
|
||||
printer.print('<').print('a')
|
||||
@@ -101,9 +111,21 @@ class GitBucketHtmlSerializer(
|
||||
printer.print('>').print(rendering.text).print("</a>")
|
||||
}
|
||||
|
||||
private def fixUrl(url: String): String = {
|
||||
if(!enableWikiLink || url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#")){
|
||||
private def fixUrl(url: String, isImage: Boolean = false): String = {
|
||||
if(url.startsWith("http://") || url.startsWith("https://") || url.startsWith("#") || url.startsWith("/")){
|
||||
url
|
||||
} else if(!enableWikiLink){
|
||||
if(context.currentPath.contains("/blob/")){
|
||||
url + (if(isImage) "?raw=true" else "")
|
||||
} else if(context.currentPath.contains("/tree/")){
|
||||
val paths = context.currentPath.split("/")
|
||||
val branch = if(paths.length > 3) paths.drop(4).mkString("/") else repository.repository.defaultBranch
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
|
||||
} else {
|
||||
val paths = context.currentPath.split("/")
|
||||
val branch = if(paths.length > 3) paths.last else repository.repository.defaultBranch
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/blob/" + branch + "/" + url + (if(isImage) "?raw=true" else "")
|
||||
}
|
||||
} else {
|
||||
repository.httpUrl.replaceFirst("/git/", "/").stripSuffix(".git") + "/wiki/_blob/" + url
|
||||
}
|
||||
@@ -130,7 +152,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) else t
|
||||
|
||||
if (abbreviations.isEmpty) {
|
||||
printer.print(text)
|
||||
@@ -138,6 +163,28 @@ class GitBucketHtmlSerializer(
|
||||
printWithAbbreviations(text)
|
||||
}
|
||||
}
|
||||
|
||||
override def visit(node: BulletListNode): Unit = {
|
||||
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
|
||||
printer.println().print("""<ul class="task-list">""").indent(+2)
|
||||
visitChildren(node)
|
||||
printer.indent(-2).println().print("</ul>")
|
||||
} else {
|
||||
printIndentedTag(node, "ul")
|
||||
}
|
||||
}
|
||||
|
||||
override def visit(node: ListItemNode): Unit = {
|
||||
if (printChildrenToString(node).contains("""class="task-list-item-checkbox" """)) {
|
||||
printer.println()
|
||||
printer.print("""<li class="task-list-item">""")
|
||||
visitChildren(node)
|
||||
printer.print("</li>")
|
||||
} else {
|
||||
printer.println()
|
||||
printTag(node, "li")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object GitBucketHtmlSerializer {
|
||||
@@ -150,4 +197,14 @@ object GitBucketHtmlSerializer {
|
||||
val noSpecialChars = StringUtil.urlEncode(normalized)
|
||||
noSpecialChars.toLowerCase(Locale.ENGLISH)
|
||||
}
|
||||
|
||||
def escapeTaskList(text: String): String = {
|
||||
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:", """<input type="checkbox" class="task-list-item-checkbox" checked="checked" """ + disabled + "/>")
|
||||
.replaceAll("task: :", """<input type="checkbox" class="task-list-item-checkbox" """ + disabled + "/>")
|
||||
}
|
||||
}
|
||||
|
@@ -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,47 @@ 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
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Format java.util.Date to "yyyy-MM-dd'T'hh:mm:ss'Z'".
|
||||
*/
|
||||
@@ -48,8 +89,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, 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,
|
||||
|
@@ -25,7 +25,7 @@
|
||||
@if(repository.repository.description.isDefined){
|
||||
<div>@repository.repository.description</div>
|
||||
}
|
||||
<div><span class="muted small">Last updated: @datetime(repository.repository.lastActivityDate)</span></div>
|
||||
<div><span class="muted small">Updated @helper.html.datetimeago(repository.repository.lastActivityDate)</span></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@@ -11,9 +11,11 @@
|
||||
<li@if(active=="system"){ class="active"}>
|
||||
<a href="@path/admin/system">System Settings</a>
|
||||
</li>
|
||||
<li@if(active=="plugins"){ class="active"}>
|
||||
<a href="@path/admin/plugins">Plugins</a>
|
||||
</li>
|
||||
@if(service.SystemSettingsService.enablePluginSystem){
|
||||
<li@if(active=="plugins"){ class="active"}>
|
||||
<a href="@path/admin/plugins">Plugins</a>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a href="@path/console/login.jsp">H2 Console</a>
|
||||
</li>
|
||||
|
@@ -31,6 +31,14 @@
|
||||
You can use this property to adjust URL difference between the reverse proxy and GitBucket.
|
||||
</p>
|
||||
<!--====================================================================-->
|
||||
<!-- Information -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
<label><span class="strong">Information</span> (HTML is available)</label>
|
||||
<fieldset>
|
||||
<textarea name="information" style="width: 600px; height: 100px;">@settings.information</textarea>
|
||||
</fieldset>
|
||||
<!--====================================================================-->
|
||||
<!-- Account registration -->
|
||||
<!--====================================================================-->
|
||||
<hr>
|
||||
|
@@ -16,6 +16,9 @@
|
||||
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
|
||||
Disable
|
||||
</label>
|
||||
<div>
|
||||
<span id="error-removed" class="error"></span>
|
||||
</div>
|
||||
}
|
||||
</fieldset>
|
||||
@if(account.map(_.password.nonEmpty).getOrElse(true)){
|
||||
|
74
src/main/twirl/dashboard/header.scala.html
Normal file
@@ -0,0 +1,74 @@
|
||||
@(openCount: Int,
|
||||
closedCount: Int,
|
||||
condition: service.IssuesService.IssueSearchCondition,
|
||||
groups: List[String])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<span class="small">
|
||||
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
|
||||
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
|
||||
@openCount Open
|
||||
</a>
|
||||
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
|
||||
<img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/>
|
||||
@closedCount Closed
|
||||
</a>
|
||||
</span>
|
||||
<div class="pull-right" id="table-issues-control">
|
||||
@helper.html.dropdown("Visibility", flat = true){
|
||||
<li>
|
||||
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("private")) None else Some("private"))).toURL)">
|
||||
@helper.html.checkicon(condition.visibility == Some("private"))
|
||||
Private repository only
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@(condition.copy(visibility = (if(condition.visibility == Some("public")) None else Some("public"))).toURL)">
|
||||
@helper.html.checkicon(condition.visibility == Some("public"))
|
||||
Public repository only
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@helper.html.dropdown("Organization", flat = true){
|
||||
@groups.map { group =>
|
||||
<li>
|
||||
<a href="@((if(condition.groups.contains(group)) condition.copy(groups = condition.groups - group) else condition.copy(groups = condition.groups + group)).toURL)">
|
||||
@helper.html.checkicon(condition.groups.contains(group))
|
||||
@avatar(group, 20) @group
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Sort", flat = true){
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</div>
|
@@ -1,50 +1,16 @@
|
||||
@(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)
|
||||
filter: String,
|
||||
groups: List[String])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Your Issues"){
|
||||
<div class="container">
|
||||
@html.main("Issues"){
|
||||
@dashboard.html.tab("issues")
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter == "all"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/repos@condition.toURL">
|
||||
<span class="count-right">@allCount</span>
|
||||
In your repositories
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "assigned"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/assigned@condition.toURL">
|
||||
<span class="count-right">@assignedCount</span>
|
||||
Assigned to you
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/issues/created_by@condition.toURL">
|
||||
<span class="count-right">@createdByCount</span>
|
||||
Created by you
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<ul class="nav nav-pills nav-stacked small">
|
||||
@repositories.map { case (owner, name, count) =>
|
||||
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
|
||||
<a href="@condition.copy(repo = Some(owner + "/" + name)).toURL">
|
||||
<span class="count-right">@count</span>
|
||||
@owner/@name
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@listparts
|
||||
<div class="container">
|
||||
@issuesnavi(filter, "issues", condition)
|
||||
@issueslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
67
src/main/twirl/dashboard/issueslist.scala.html
Normal file
@@ -0,0 +1,67 @@
|
||||
@(issues: List[service.IssuesService.IssueInfo],
|
||||
page: Int,
|
||||
openCount: Int,
|
||||
closedCount: Int,
|
||||
condition: service.IssuesService.IssueSearchCondition,
|
||||
filter: String,
|
||||
groups: List[String])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@import service.IssuesService.IssueInfo
|
||||
@*
|
||||
<ul class="nav nav-pills-group pull-left fill-width">
|
||||
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/issues/created_by@condition.toURL">Created</a></li>
|
||||
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/issues/assigned@condition.toURL">Assigned</a></li>
|
||||
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/issues/mentioned@condition.toURL">Mentioned</a></li>
|
||||
</ul>
|
||||
*@
|
||||
<table class="table table-bordered table-hover table-issues">
|
||||
<tr>
|
||||
<th style="background-color: #eee;">
|
||||
@dashboard.html.header(openCount, closedCount, condition, groups)
|
||||
</th>
|
||||
</tr>
|
||||
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
|
||||
<tr>
|
||||
<td style="padding-top: 15px; padding-bottom: 15px;">
|
||||
@if(issue.isPullRequest){
|
||||
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
|
||||
} else {
|
||||
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
|
||||
}
|
||||
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a> ・
|
||||
@if(issue.isPullRequest){
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
||||
} else {
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
|
||||
}
|
||||
@labels.map { label =>
|
||||
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
|
||||
}
|
||||
<span class="pull-right muted">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@avatar(userName, 20, tooltip = true)
|
||||
}
|
||||
@if(commentCount > 0){
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
|
||||
<img src="@assets/common/images/comment-active.png"> @commentCount
|
||||
</a>
|
||||
} else {
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
|
||||
<img src="@assets/common/images/comment.png"> @commentCount
|
||||
</a>
|
||||
}
|
||||
</span>
|
||||
<div class="small muted" style="margin-left: 20px; margin-top: 5px;">
|
||||
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||
@milestone.map { milestone =>
|
||||
<span style="margin: 20px;"><a href="@condition.copy(milestoneId = Some(Some(1))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<div class="pull-right">
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
|
||||
</div>
|
22
src/main/twirl/dashboard/issuesnavi.scala.html
Normal file
@@ -0,0 +1,22 @@
|
||||
@(filter: String,
|
||||
active: String,
|
||||
condition: service.IssuesService.IssueSearchCondition)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<ul class="nav nav-pills-group pull-left fill-width">
|
||||
<li class="@if(filter == "created_by"){active} first">
|
||||
<a href="@path/dashboard/@active/created_by@condition.copy(author = None, assigned = None).toURL">Created</a>
|
||||
</li>
|
||||
<li class="@if(filter == "assigned"){active}">
|
||||
<a href="@path/dashboard/@active/assigned@condition.copy(author = None, assigned = None).toURL">Assigned</a>
|
||||
</li>
|
||||
<li class="@if(filter == "mentioned"){active} last">
|
||||
<a href="@path/dashboard/@active/mentioned@condition.copy(author = None, assigned = None).toURL">Mentioned</a>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<form method="GET" id="search-filter-form" action="@path/dashboard/@active" style="margin-bottom: 0px;">
|
||||
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px; width: 400px;"
|
||||
value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
@@ -1,42 +1,16 @@
|
||||
@(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)
|
||||
filter: String,
|
||||
groups: List[String])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Your Issues"){
|
||||
<div class="container">
|
||||
@html.main("Pull Requests"){
|
||||
@dashboard.html.tab("pulls")
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter == "created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/owned@condition.toURL">
|
||||
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
|
||||
Yours
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "not_created_by"){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/public@condition.toURL">
|
||||
<span class="count-right">@counts.filter(_.userName != loginAccount.get.userName).map(_.count).sum</span>
|
||||
Public
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<hr/>
|
||||
<ul class="nav nav-pills nav-stacked small">
|
||||
@repositories.map { case (owner, name, count) =>
|
||||
<li@if(condition.repo == Some(owner + "/" + name)){ class="active"}>
|
||||
<a href="@path/dashboard/pulls/for/@owner/@name">
|
||||
<span class="count-right">@count</span>
|
||||
@owner/@name
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@listparts
|
||||
<div class="container">
|
||||
@issuesnavi(filter, "pulls", condition)
|
||||
@pullslist(issues, page, openCount, closedCount, condition, filter, groups)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
67
src/main/twirl/dashboard/pullslist.scala.html
Normal file
@@ -0,0 +1,67 @@
|
||||
@(issues: List[service.IssuesService.IssueInfo],
|
||||
page: Int,
|
||||
openCount: Int,
|
||||
closedCount: Int,
|
||||
condition: service.IssuesService.IssueSearchCondition,
|
||||
filter: String,
|
||||
groups: List[String])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@import service.IssuesService.IssueInfo
|
||||
@*
|
||||
<ul class="nav nav-pills-group pull-left fill-width">
|
||||
<li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/pulls/created_by@condition.toURL">Created</a></li>
|
||||
<li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/pulls/assigned@condition.toURL">Assigned</a></li>
|
||||
<li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/pulls/mentioned@condition.toURL">Mentioned</a></li>
|
||||
<li class="pull-right">
|
||||
<div class="input-prepend" style="margin-bottom: 0px;">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn dropdown-toggle" data-toggle="dropdown" style="height: 34px;">
|
||||
Filter
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="?q=is:open">Open issues and pull requests</a></li>
|
||||
<li><a href="?q=is:open+is:issue+author:@urlEncode(loginAccount.get.userName)">Your issues</a></li>
|
||||
<li><a href="?q=is:open+is:pr+author:@urlEncode(loginAccount.get.userName)">Your pull requests</a></li>
|
||||
<li><a href="?q=is:open+assignee:@urlEncode(loginAccount.get.userName)">Everything assigned to you</a></li>
|
||||
<li><a href="?q=is:open+mentions:@urlEncode(loginAccount.get.userName)">Everything mentioning you</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
*@
|
||||
<table class="table table-bordered table-hover table-issues">
|
||||
<tr>
|
||||
<th style="background-color: #eee;">
|
||||
@dashboard.html.header(openCount, closedCount, condition, groups)
|
||||
</th>
|
||||
</tr>
|
||||
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
||||
<span class="pull-right muted">#@issue.issueId</span>
|
||||
<div style="margin-left: 20px;">
|
||||
@issue.content.map { content =>
|
||||
@cut(content, 90)
|
||||
}.getOrElse {
|
||||
<span class="muted">No description available</span>
|
||||
}
|
||||
</div>
|
||||
<div class="small muted" style="margin-left: 20px;">
|
||||
@avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||
@if(commentCount > 0){
|
||||
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<div class="pull-right">
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL)
|
||||
</div>
|
@@ -1,13 +1,47 @@
|
||||
@(active: String = "")(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<ul class="nav nav-tabs">
|
||||
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li>
|
||||
@if(loginAccount.isDefined){
|
||||
<li@if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li>
|
||||
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li>
|
||||
}
|
||||
@if(active == ""){
|
||||
<li class="pull-right"><a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a></li>
|
||||
}
|
||||
</ul>
|
||||
<div class="dashboard-nav">
|
||||
<div class="container">
|
||||
<a href="@path/" @if(active == ""){ class="active"}>
|
||||
<img src="@assets/common/images/menu-feed.png">
|
||||
News Feed
|
||||
</a>
|
||||
@if(loginAccount.isDefined){
|
||||
<a href="@path/dashboard/pulls" @if(active == "pulls" ){ class="active"}>
|
||||
<img src="@assets/common/images/menu-pulls.png">
|
||||
Pull Requests
|
||||
</a>
|
||||
<a href="@path/dashboard/issues" @if(active == "issues"){ class="active"}>
|
||||
<img src="@assets/common/images/menu-issues.png">
|
||||
Issues
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<style type="text/css">
|
||||
div.dashboard-nav {
|
||||
border-bottom: 1px solid #ddd;
|
||||
text-align: right;
|
||||
height: 32px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
div.dashboard-nav a {
|
||||
line-height: 10px;
|
||||
margin-left: 20px;
|
||||
padding-bottom: 13px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
div.dashboard-nav a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
div.dashboard-nav a.active {
|
||||
border-bottom: 2px solid #bb4444;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
@@ -62,7 +62,7 @@
|
||||
@detailActivity(activity: model.Activity, image: String) = {
|
||||
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
|
||||
<div class="activity-content">
|
||||
<div class="muted small">@datetime(activity.activityDate)</div>
|
||||
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
||||
<div class="strong">
|
||||
@avatar(activity.activityUserName, 16)
|
||||
@activityMessage(activity.message)
|
||||
@@ -76,7 +76,7 @@
|
||||
@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
|
||||
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
|
||||
<div class="activity-content">
|
||||
<div class="muted small">@datetime(activity.activityDate)</div>
|
||||
<div class="muted small">@helper.html.datetimeago(activity.activityDate)</div>
|
||||
<div class="strong">
|
||||
@avatar(activity.activityUserName, 16)
|
||||
@activityMessage(activity.message)
|
||||
@@ -91,7 +91,7 @@
|
||||
<div>
|
||||
@avatar(activity.activityUserName, 16)
|
||||
@activityMessage(activity.message)
|
||||
<span class="muted small">@datetime(activity.activityDate)</span>
|
||||
<span class="muted small">@helper.html.datetimeago(activity.activityDate)</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
62
src/main/twirl/helper/branchcontrol.scala.html
Normal file
@@ -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
|
||||
) {
|
||||
<li><div id="branch-control-title">Switch branches<button id="branch-control-close" class="pull-right">×</button></div></li>
|
||||
<li><input id="branch-control-input" type="text" placeholder="Find or create branch ..."/></li>
|
||||
@body
|
||||
@if(hasWritePermission) {
|
||||
<li id="create-branch" style="display: none;">
|
||||
<a><form action="@url(repository)/branches" method="post" style="margin: 0;">
|
||||
<span class="new-branch-name">Create branch: <span class="new-branch"></span></span>
|
||||
<br><span style="padding-left: 17px;">from '@branch'</span>
|
||||
<input type="hidden" name="new">
|
||||
<input type="hidden" name="from" value="@branch">
|
||||
</form></a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#branch-control-input').parent().click(function(e) {
|
||||
e.stopPropagation();
|
||||
});
|
||||
$('#branch-control-close').click(function() {
|
||||
$('[data-toggle="dropdown"]').parent().removeClass('open');
|
||||
});
|
||||
$('#branch-control-input').keyup(function() {
|
||||
var inputVal = $('#branch-control-input').val();
|
||||
$.each($('#branch-control-input').parent().parent().find('a'), function(index, elem) {
|
||||
if (!inputVal || !elem.text.trim() || elem.text.trim().lastIndexOf(inputVal, 0) >= 0) {
|
||||
$(elem).parent().show();
|
||||
} else {
|
||||
$(elem).parent().hide();
|
||||
}
|
||||
});
|
||||
@if(hasWritePermission) {
|
||||
if (inputVal) {
|
||||
$('#create-branch').parent().find('li:last-child').show().find('.new-branch').text(inputVal);
|
||||
} else {
|
||||
$('#create-branch').parent().find('li:last-child').hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
@if(hasWritePermission) {
|
||||
$('#create-branch').click(function() {
|
||||
$(this).find('input[name="new"]').val($('.dropdown-menu input').val())
|
||||
$(this).find('form').submit()
|
||||
});
|
||||
}
|
||||
$('.btn-group').click(function() {
|
||||
$('#branch-control-input').val('');
|
||||
$('.dropdown-menu li').show();
|
||||
$('#create-branch').hide();
|
||||
});
|
||||
});
|
||||
</script>
|
10
src/main/twirl/helper/datetimeago.scala.html
Normal file
@@ -0,0 +1,10 @@
|
||||
@(latestUpdatedDate: java.util.Date,
|
||||
recentOnly: Boolean = true)
|
||||
@import view.helpers._
|
||||
<span data-toggle="tooltip" title="@datetime(latestUpdatedDate)">
|
||||
@if(recentOnly){
|
||||
@datetimeAgoRecentOnly(latestUpdatedDate)
|
||||
}else{
|
||||
@datetimeAgo(latestUpdatedDate)
|
||||
}
|
||||
</span>
|
@@ -9,9 +9,12 @@
|
||||
@if(showIndex){
|
||||
<div>
|
||||
<div class="pull-right" style="margin-bottom: 10px;">
|
||||
<input id="toggle-file-list" type="button" class="btn" value="Show file list"/>
|
||||
<div class="btn-group" data-toggle="buttons-radio">
|
||||
<input type="button" id="btn-unified" class="btn btn-default btn-small active" value="Unified">
|
||||
<input type="button" id="btn-split" class="btn btn-default btn-small" value="Split">
|
||||
</div>
|
||||
</div>
|
||||
Showing @diffs.size changed @plural(diffs.size, "file")
|
||||
Showing <a href="javascript:void(0);" id="toggle-file-list">@diffs.size changed @plural(diffs.size, "file")</a>
|
||||
</div>
|
||||
<ul id="commit-file-list" style="display: none;">
|
||||
@diffs.zipWithIndex.map { case (diff, i) =>
|
||||
@@ -38,7 +41,7 @@
|
||||
<a name="diff-@i"></a>
|
||||
<table class="table table-bordered">
|
||||
<tr>
|
||||
<th style="font-weight: normal;" class="box-header">
|
||||
<th style="font-weight: normal; line-height: 27px;" class="box-header">
|
||||
@if(diff.changeType == ChangeType.COPY || diff.changeType == ChangeType.RENAME){
|
||||
@diff.oldPath -> @diff.newPath
|
||||
@if(newCommitId.isDefined){
|
||||
@@ -66,7 +69,7 @@
|
||||
</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<td style="padding: 0;">
|
||||
@if(diff.newContent != None || diff.oldContent != None){
|
||||
<div id="diffText-@i"></div>
|
||||
<textarea id="newText-@i" style="display: none;">@diff.newContent.getOrElse("")</textarea>
|
||||
@@ -94,10 +97,25 @@ $(function(){
|
||||
});
|
||||
}
|
||||
|
||||
@diffs.zipWithIndex.map { case (diff, i) =>
|
||||
@if(diff.newContent != None || diff.oldContent != None){
|
||||
if($('#oldText-@i').length > 0){
|
||||
diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i');
|
||||
// Render diffs as unified mode initially
|
||||
renderDiffs(1);
|
||||
|
||||
$('#btn-unified').click(function(){
|
||||
$('.container-wide').removeClass('container-wide').addClass('container');
|
||||
renderDiffs(1);
|
||||
});
|
||||
|
||||
$('#btn-split').click(function(){
|
||||
$('.container').removeClass('container').addClass('container-wide');
|
||||
renderDiffs(0);
|
||||
});
|
||||
|
||||
function renderDiffs(viewType){
|
||||
@diffs.zipWithIndex.map { case (diff, i) =>
|
||||
@if(diff.newContent != None || diff.oldContent != None){
|
||||
if($('#oldText-@i').length > 0){
|
||||
diffUsingJS('oldText-@i', 'newText-@i', 'diffText-@i', viewType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,13 @@
|
||||
@(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "", right: Boolean = false)(body: Html)
|
||||
<div class="btn-group"@if(style.nonEmpty){ style="@style"}>
|
||||
<button class="btn dropdown-toggle@if(mini){ btn-mini} else { btn-small}" data-toggle="dropdown">
|
||||
@(value : String = "",
|
||||
prefix: String = "",
|
||||
mini : Boolean = true,
|
||||
style : String = "",
|
||||
right : Boolean = false,
|
||||
flat : Boolean = false)(body: Html)
|
||||
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
|
||||
<button
|
||||
@if(flat){style="border: none; background-color: #eee;"}
|
||||
class="dropdown-toggle @if(!flat){btn} else {flat} @if(mini){btn-mini} else {btn-small}" data-toggle="dropdown">
|
||||
@if(value.isEmpty){
|
||||
<i class="icon-cog"></i>
|
||||
} else {
|
||||
|
7
src/main/twirl/helper/error.scala.html
Normal file
@@ -0,0 +1,7 @@
|
||||
@(error: Option[Any])
|
||||
@if(error.isDefined){
|
||||
<div class='alert alert-danger'>
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
@error
|
||||
</div>
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
@(info: Option[Any])
|
||||
@if(info.isDefined){
|
||||
<div class="alert alert-info">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
@info
|
||||
</div>
|
||||
}
|
||||
|
@@ -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, hasWritePermission: Boolean,
|
||||
style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@@ -38,7 +38,8 @@ $(function(){
|
||||
$.post('@url(repository)/_preview', {
|
||||
content : $('#content').val(),
|
||||
enableWikiLink : @enableWikiLink,
|
||||
enableRefsLink : @enableRefsLink
|
||||
enableRefsLink : @enableRefsLink,
|
||||
enableTaskList : @enableTaskList
|
||||
}, function(data){
|
||||
$('#preview-area').html(data);
|
||||
prettyPrint();
|
||||
|
@@ -4,13 +4,23 @@
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@main("GitBucket"){
|
||||
@dashboard.html.tab()
|
||||
<div class="container">
|
||||
@dashboard.html.tab()
|
||||
<div class="row-fluid">
|
||||
<div class="span8">
|
||||
<div class="pull-right">
|
||||
<a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a>
|
||||
</div>
|
||||
@helper.html.activities(activities)
|
||||
</div>
|
||||
|
||||
<div class="span4">
|
||||
@settings.information.map { information =>
|
||||
<div class="alert alert-info" style="background-color: white; color: #555; border-color: #4183c4; font-size: small; line-height: 120%;">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
@Html(information)
|
||||
</div>
|
||||
}
|
||||
@if(loginAccount.isEmpty){
|
||||
@signinform(settings)
|
||||
} else {
|
||||
|
@@ -5,11 +5,12 @@
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@if(loginAccount.isDefined){
|
||||
<hr/><br/>
|
||||
<form method="POST" validate="true">
|
||||
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
|
||||
<div class="box issue-comment-box">
|
||||
<div class="box-content">
|
||||
@helper.html.preview(repository, "", false, 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)
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
@@ -27,4 +28,4 @@ $(function(){
|
||||
$('<input type="hidden">').attr('name', 'action').val($(this).val().toLowerCase()).appendTo('form');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
@@ -5,20 +5,36 @@
|
||||
pullreq: Option[model.PullRequest] = None)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
|
||||
<div class="box issue-comment-box">
|
||||
<div class="box-header-small">
|
||||
@user(issue.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.registeredDate)</span>
|
||||
<span class="pull-right">
|
||||
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
||||
<a href="#" data-issue-id="@issue.issueId"><i class="icon-pencil"></i></a>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="box-content issue-content" id="issueContent">
|
||||
@markdown(issue.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@comments.map { comment =>
|
||||
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
|
||||
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
|
||||
<div class="box issue-comment-box" id="comment-@comment.commentId">
|
||||
<div class="box-header-small">
|
||||
<i class="icon-comment"></i>
|
||||
@user(comment.commentedUserName, styleClass="username strong")
|
||||
@if(comment.action == "comment"){
|
||||
commented
|
||||
} else {
|
||||
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
|
||||
}
|
||||
<span class="muted">
|
||||
@if(comment.action == "comment"){
|
||||
commented
|
||||
} else {
|
||||
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
|
||||
}
|
||||
@helper.html.datetimeago(comment.registeredDate)
|
||||
</span>
|
||||
<span class="pull-right">
|
||||
@datetime(comment.registeredDate)
|
||||
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" &&
|
||||
(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
|
||||
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>
|
||||
@@ -30,7 +46,7 @@
|
||||
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
|
||||
@defining(comment.content.substring(comment.content.length - 40)){ id =>
|
||||
<div class="pull-right"><a href="@path/@repository.owner/@repository.name/commit/@id" class="monospace">@id.substring(0, 7)</a></div>
|
||||
@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, hasWritePermission)
|
||||
}
|
||||
} else {
|
||||
@if(comment.action == "refer"){
|
||||
@@ -38,7 +54,7 @@
|
||||
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong>
|
||||
}
|
||||
} else {
|
||||
@markdown(comment.content, repository, false, true)
|
||||
@markdown(comment.content, repository, false, true, true, hasWritePermission)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -52,9 +68,9 @@
|
||||
@if(pullreq.get.requestUserName == repository.owner){
|
||||
<span class="label label-info monospace">@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span>
|
||||
} else {
|
||||
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
|
||||
<span class="label label-info monospace">@pullreq.map(_.userName):@pullreq.map(_.branch)</span> from <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
|
||||
}
|
||||
@datetime(comment.registeredDate)
|
||||
@helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "close" || comment.action == "close_comment"){
|
||||
@@ -62,9 +78,9 @@
|
||||
<span class="label label-important">Closed</span>
|
||||
@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)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -72,27 +88,36 @@
|
||||
<div class="small issue-comment-action">
|
||||
<span class="label label-success">Reopened</span>
|
||||
@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)
|
||||
</div>
|
||||
}
|
||||
@if(comment.action == "delete_branch"){
|
||||
<div class="small issue-comment-action">
|
||||
<span class="label">Deleted</span>
|
||||
@avatar(comment.commentedUserName, 20)
|
||||
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @datetime(comment.registeredDate)
|
||||
@user(comment.commentedUserName, styleClass="username strong") deleted the <span class="label label-info monospace">@pullreq.map(_.requestBranch)</span> branch @helper.html.datetimeago(comment.registeredDate)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('i.icon-pencil').click(function(){
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
$.get('@url(repository)/issue_comments/_data/' + id,
|
||||
var id = $(this).closest('a').data('comment-id');
|
||||
var url = '@url(repository)/issue_comments/_data/' + id;
|
||||
var $content = $('#commentContent-' + id);
|
||||
|
||||
if(!id){
|
||||
id = $(this).closest('a').data('issue-id');
|
||||
url = '@url(repository)/issues/_data/' + id;
|
||||
$content = $('#issueContent');
|
||||
}
|
||||
|
||||
$.get(url,
|
||||
{
|
||||
dataType : 'html'
|
||||
},
|
||||
function(data){
|
||||
$('#commentContent-' + id).empty().html(data);
|
||||
$content.empty().html(data);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
@@ -109,5 +134,67 @@ $(function(){
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
var extractMarkdown = function(data){
|
||||
$('body').append('<div id="tmp"></div>');
|
||||
$('#tmp').html(data);
|
||||
var markdown = $('#tmp textarea').val();
|
||||
$('#tmp').remove();
|
||||
return markdown;
|
||||
};
|
||||
|
||||
var replaceTaskList = function(issueContentHtml, checkboxes) {
|
||||
var ss = [],
|
||||
markdown = extractMarkdown(issueContentHtml),
|
||||
xs = markdown.split(/- \[[x| ]\]/g);
|
||||
for (var i=0; i<xs.length; i++) {
|
||||
ss.push(xs[i]);
|
||||
if (checkboxes.eq(i).prop('checked')) ss.push('- [x]');
|
||||
else ss.push('- [ ]');
|
||||
}
|
||||
ss.pop();
|
||||
return ss.join('');
|
||||
};
|
||||
|
||||
$('#issueContent').on('click', ':checkbox', function(ev){
|
||||
var checkboxes = $('#issueContent :checkbox');
|
||||
$.get('@url(repository)/issues/_data/@issue.issueId',
|
||||
{
|
||||
dataType : 'html'
|
||||
},
|
||||
function(responseContent){
|
||||
$.ajax({
|
||||
url: '@url(repository)/issues/edit/@issue.issueId',
|
||||
type: 'POST',
|
||||
data: {
|
||||
title : $('#issueTitle').text(),
|
||||
content : replaceTaskList(responseContent, checkboxes)
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
$('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(responseContent){
|
||||
$.ajax({
|
||||
url: '@url(repository)/issue_comments/edit/' + commentId,
|
||||
type: 'POST',
|
||||
data: {
|
||||
issueId : 0,
|
||||
content : replaceTaskList(responseContent, checkboxes)
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
@@ -7,7 +7,8 @@
|
||||
@import view.helpers._
|
||||
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.menu("issues", repository){
|
||||
@tab("", true, repository)
|
||||
@navigation("issues", false, repository)
|
||||
<br/><br/><hr style="margin-bottom: 10px;">
|
||||
<form action="@url(repository)/issues/new" method="POST" validate="true">
|
||||
<div class="row-fluid">
|
||||
<div class="span9">
|
||||
@@ -32,7 +33,7 @@
|
||||
@if(hasWritePermission){
|
||||
<input type="hidden" name="milestoneId" value=""/>
|
||||
@helper.html.dropdown() {
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li>
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
|
||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
||||
@@ -40,9 +41,9 @@
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
|
||||
<img src="@assets/common/images/alert.png"/><span class="milestone-alert">Due by @date(dueDate)</span>
|
||||
} else {
|
||||
<span class="muted">Due in @date(dueDate)</span>
|
||||
<span class="muted">Due by @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
@@ -56,7 +57,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
@helper.html.preview(repository, "", false, 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)
|
||||
</div>
|
||||
</div>
|
||||
<div class="pull-right">
|
||||
@@ -65,7 +66,7 @@
|
||||
</div>
|
||||
<div class="span3">
|
||||
@if(hasWritePermission){
|
||||
<span class="strong">Add Labels</span>
|
||||
<span class="strong">Labels</span>
|
||||
<div>
|
||||
<div id="label-list">
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@@ -112,7 +113,7 @@ $(function(){
|
||||
if(milestoneId == ''){
|
||||
$('#label-milestone').text('No milestone');
|
||||
} else {
|
||||
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
|
||||
$('#label-milestone').html($('<span class="strong">').text(title));
|
||||
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
|
||||
}
|
||||
$('input[name=milestoneId]').val(milestoneId);
|
||||
|
@@ -5,8 +5,8 @@
|
||||
<textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea>
|
||||
}
|
||||
<div>
|
||||
<input type="button" id="update-comment-@commentId" class="btn btn-small" value="Update Comment"/>
|
||||
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger pull-right" value="Cancel"/>
|
||||
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger" value="Cancel"/>
|
||||
<input type="button" id="update-comment-@commentId" class="btn btn-small pull-right" value="Update comment"/>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
|
@@ -1,42 +1,35 @@
|
||||
@(title: String, content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context)
|
||||
@(content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context)
|
||||
@import context._
|
||||
<span id="error-edit-title" class="error"></span>
|
||||
<input type="text" style="width: 635px;" id="edit-title" value="@title"/>
|
||||
@helper.html.attached(owner, repository){
|
||||
<textarea style="width: 635px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
|
||||
}
|
||||
<div>
|
||||
<input type="button" id="update" class="btn btn-small" value="Update Issue"/>
|
||||
<input type="button" id="cancel" class="btn btn-small btn-danger pull-right" value="Cancel"/>
|
||||
<input type="button" id="cancel-issue" class="btn btn-small btn-danger" value="Cancel"/>
|
||||
<input type="button" id="update-issue" class="btn btn-small pull-right" value="Update comment"/>
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#edit-content').elastic();
|
||||
|
||||
var callback = function(data){
|
||||
$('#update, #cancel').removeAttr('disabled');
|
||||
$('#issueTitle').empty().text(data.title);
|
||||
$('#issueContent').empty().html(data.content);
|
||||
};
|
||||
|
||||
$('#update').click(function(){
|
||||
$('#update-issue').click(function(){
|
||||
$('#update, #cancel').attr('disabled', 'disabled');
|
||||
$.ajax({
|
||||
url: '@path/@owner/@repository/issues/edit/@issueId',
|
||||
type: 'POST',
|
||||
data: {
|
||||
title : $('#edit-title').val(),
|
||||
content : $('#edit-content').val()
|
||||
}
|
||||
}).done(
|
||||
callback
|
||||
).fail(function(req) {
|
||||
$('#update, #cancel').removeAttr('disabled');
|
||||
$('#error-edit-title').text($.parseJSON(req.responseText).title);
|
||||
});
|
||||
});
|
||||
|
||||
$('#cancel').click(function(){
|
||||
$('#cancel-issue').click(function(){
|
||||
$('#update, #cancel').attr('disabled', 'disabled');
|
||||
$.get('@path/@owner/@repository/issues/_data/@issueId', callback);
|
||||
return false;
|
||||
|
@@ -10,31 +10,83 @@
|
||||
@import view.helpers._
|
||||
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.menu("issues", repository){
|
||||
@tab("issues", false, repository)
|
||||
<ul class="nav nav-tabs pull-left fill-width">
|
||||
<li class="pull-left"><a href="@url(repository)/issues"><i class="icon-arrow-left"></i> Back to issue list</a></li>
|
||||
<li class="pull-right">Issue #@issue.issueId</li>
|
||||
</ul>
|
||||
<div>
|
||||
<div class="show-title pull-right">
|
||||
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
||||
<a class="btn btn-small" href="#" id="edit">Edit</a>
|
||||
}
|
||||
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||
</div>
|
||||
<div class="edit-title pull-right" style="display: none;">
|
||||
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
|
||||
</div>
|
||||
<h1>
|
||||
<span class="show-title">
|
||||
<span id="show-title">@issue.title</span>
|
||||
<span class="muted">#@issue.issueId</span>
|
||||
</span>
|
||||
<span class="edit-title" style="display: none;">
|
||||
<span id="error-edit-title" class="error"></span>
|
||||
<input type="text" style="width: 700px;" id="edit-title" value="@issue.title"/>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
@if(issue.closed) {
|
||||
<span class="label label-important issue-status">Closed</span>
|
||||
} else {
|
||||
<span class="label label-success issue-status">Open</span>
|
||||
}
|
||||
<span class="muted">
|
||||
@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")
|
||||
}
|
||||
</span>
|
||||
<br/><br/>
|
||||
<hr>
|
||||
<div class="row-fluid">
|
||||
<div class="span10">
|
||||
@issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
|
||||
@commentlist(issue, comments, hasWritePermission, repository)
|
||||
@commentform(issue, true, hasWritePermission, repository)
|
||||
</div>
|
||||
<div class="span2">
|
||||
@if(issue.closed) {
|
||||
<span class="label label-important issue-status">Closed</span>
|
||||
} else {
|
||||
<span class="label label-success issue-status">Open</span>
|
||||
}
|
||||
<div class="small" style="text-align: center;">
|
||||
@defining(comments.filter( _.action.contains("comment") ).size){ count =>
|
||||
<span class="strong">@count</span> @plural(count, "comment")
|
||||
}
|
||||
</div>
|
||||
<hr/>
|
||||
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
|
||||
@issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#edit').click(function(){
|
||||
$('.edit-title').show();
|
||||
$('.show-title').hide();
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#update').click(function(){
|
||||
$(this).attr('disabled', 'disabled');
|
||||
$.ajax({
|
||||
url: '@url(repository)/issues/edit_title/@issue.issueId',
|
||||
type: 'POST',
|
||||
data: {
|
||||
title : $('#edit-title').val()
|
||||
}
|
||||
}).done(function(data){
|
||||
$('#show-title').empty().text(data.title);
|
||||
$('#cancel').click();
|
||||
$(this).removeAttr('disabled');
|
||||
}).fail(function(req){
|
||||
$(this).removeAttr('disabled');
|
||||
$('#error-edit-title').text($.parseJSON(req.responseText).title);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#cancel').click(function(){
|
||||
$('.edit-title').hide();
|
||||
$('.show-title').show();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
@@ -1,145 +0,0 @@
|
||||
@(issue: model.Issue,
|
||||
comments: List[model.IssueComment],
|
||||
collaborators: List[String],
|
||||
milestones: List[(model.Milestone, Int, Int)],
|
||||
hasWritePermission: Boolean,
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
|
||||
<div class="box issue-box">
|
||||
<div class="box-content" style="padding: 0px;">
|
||||
<div class="issue-header">
|
||||
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
||||
<span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span>
|
||||
}
|
||||
<div class="small muted">
|
||||
@user(issue.openedUserName, styleClass="username strong") opened this issue @datetime(issue.registeredDate)
|
||||
</div>
|
||||
<h4 id="issueTitle">@issue.title</h4>
|
||||
</div>
|
||||
<div class="issue-info">
|
||||
<span id="label-assigned">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@avatar(userName, 20) @user(userName, styleClass="username strong") is assigned
|
||||
}.getOrElse("No one is assigned")
|
||||
</span>
|
||||
@if(hasWritePermission){
|
||||
@helper.html.dropdown() {
|
||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
|
||||
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
}
|
||||
<div class="pull-right">
|
||||
<span id="label-milestone">
|
||||
@issue.milestoneId.map { milestoneId =>
|
||||
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
|
||||
Milestone: <span class="strong">@milestone.title</span>
|
||||
}
|
||||
}.getOrElse("No milestone")
|
||||
</span>
|
||||
<div id="milestone-progress-area">
|
||||
@issue.milestoneId.map { milestoneId =>
|
||||
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
|
||||
@issues.milestones.html.progress(openCount + closeCount, closeCount, false)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if(hasWritePermission){
|
||||
@helper.html.dropdown() {
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
|
||||
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
||||
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
|
||||
} else {
|
||||
<span class="muted">Due in @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="issue-content" id="issueContent">
|
||||
@markdown(issue.content getOrElse "No description given.", repository, false, true)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="issue-participants">
|
||||
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
||||
<span class="strong">@participants.size</span> @plural(participants.size, "participant")
|
||||
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#edit').click(function(){
|
||||
$.get('@url(repository)/issues/_data/@issue.issueId',
|
||||
{
|
||||
dataType : 'html'
|
||||
},
|
||||
function(data){
|
||||
$('#issueContent').empty().html(data);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('a.assign').click(function(){
|
||||
var $this = $(this);
|
||||
var userName = $this.data('name');
|
||||
$.post('@url(repository)/issues/@issue.issueId/assign',
|
||||
{
|
||||
assignedUserName: userName
|
||||
},
|
||||
function(){
|
||||
$('a.assign i.icon-ok').attr('class', 'icon-white');
|
||||
if(userName == ''){
|
||||
$('#label-assigned').text('No one is assigned');
|
||||
} else {
|
||||
$('#label-assigned').empty()
|
||||
.append($this.find('img.avatar').clone(false)).append(' ')
|
||||
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
|
||||
.append(' is assigned');
|
||||
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('a.milestone').click(function(){
|
||||
var title = $(this).data('title');
|
||||
var milestoneId = $(this).data('id');
|
||||
$.post('@url(repository)/issues/@issue.issueId/milestone',
|
||||
{
|
||||
milestoneId: milestoneId
|
||||
},
|
||||
function(data){
|
||||
console.log(data);
|
||||
$('a.milestone i.icon-ok').attr('class', 'icon-white');
|
||||
if(milestoneId == ''){
|
||||
$('#label-milestone').text('No milestone');
|
||||
$('#milestone-progress-area').empty();
|
||||
} else {
|
||||
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
|
||||
$('#milestone-progress-area').html(data);
|
||||
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
173
src/main/twirl/issues/issueinfo.scala.html
Normal file
@@ -0,0 +1,173 @@
|
||||
@(issue: model.Issue,
|
||||
comments: List[model.IssueComment],
|
||||
issueLabels: List[model.Label],
|
||||
collaborators: List[String],
|
||||
milestones: List[(model.Milestone, Int, Int)],
|
||||
labels: List[model.Label],
|
||||
hasWritePermission: Boolean,
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import view.helpers._
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="muted small strong">Labels</span>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
@helper.html.dropdown(right = true) {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="#" class="toggle-label" data-label-id="@label.labelId">
|
||||
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
|
||||
<span class="label" style="background-color: #@label.color;"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labellist(issueLabels)
|
||||
</ul>
|
||||
<hr/>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="muted small strong">Milestone</span>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
@helper.html.dropdown(right = true) {
|
||||
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
|
||||
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
|
||||
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert.png"/><span class="milestone-alert">Due by @date(dueDate)</span>
|
||||
} else {
|
||||
<span class="muted">Due by @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div id="milestone-progress-area">
|
||||
@issue.milestoneId.map { milestoneId =>
|
||||
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
|
||||
@issues.milestones.html.progress(openCount + closeCount, closeCount)
|
||||
}
|
||||
}
|
||||
</div>
|
||||
<span id="label-milestone">
|
||||
@issue.milestoneId.map { milestoneId =>
|
||||
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
|
||||
<span class="strong small">@milestone.title</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted small">No milestone</span>
|
||||
}
|
||||
</span>
|
||||
<hr/>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="muted small strong">Assignee</span>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
@helper.html.dropdown(right = true) {
|
||||
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
|
||||
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<span id="label-assigned">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@avatar(userName, 20) @user(userName, styleClass="username strong small")
|
||||
}.getOrElse{
|
||||
<span class="muted small">No one</span>
|
||||
}
|
||||
</span>
|
||||
<hr/>
|
||||
<div style="margin-bottom: 8px;">
|
||||
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
|
||||
<div class="muted small strong">@participants.size @plural(participants.size, "participant")</div>
|
||||
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
|
||||
}
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
$('a.toggle-label').click(function(){
|
||||
var path, icon;
|
||||
var i = $(this).children('i');
|
||||
if(i.hasClass('icon-ok')){
|
||||
path = 'delete';
|
||||
icon = 'icon-white';
|
||||
} else {
|
||||
path = 'new';
|
||||
icon = 'icon-ok';
|
||||
}
|
||||
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
|
||||
{
|
||||
labelId : $(this).data('label-id')
|
||||
},
|
||||
function(data){
|
||||
i.removeClass().addClass(icon);
|
||||
$('ul.label-list').empty().html(data);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('a.milestone').click(function(){
|
||||
var title = $(this).data('title');
|
||||
var milestoneId = $(this).data('id');
|
||||
$.post('@url(repository)/issues/@issue.issueId/milestone',
|
||||
{
|
||||
milestoneId: milestoneId
|
||||
},
|
||||
function(data){
|
||||
console.log(data);
|
||||
$('a.milestone i.icon-ok').attr('class', 'icon-white');
|
||||
if(milestoneId == ''){
|
||||
$('#label-milestone').html($('<span class="muted small">').text('No milestone'));
|
||||
$('#milestone-progress-area').empty();
|
||||
} else {
|
||||
$('#label-milestone').html($('<span class="strong small">').text(title));
|
||||
$('#milestone-progress-area').html(data);
|
||||
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('a.assign').click(function(){
|
||||
var $this = $(this);
|
||||
var userName = $this.data('name');
|
||||
$.post('@url(repository)/issues/@issue.issueId/assign',
|
||||
{
|
||||
assignedUserName: userName
|
||||
},
|
||||
function(){
|
||||
$('a.assign i.icon-ok').attr('class', 'icon-white');
|
||||
if(userName == ''){
|
||||
$('#label-assigned').html($('<span class="muted small">').text('No one'));
|
||||
} else {
|
||||
$('#label-assigned').empty()
|
||||
.append($this.find('img.avatar-mini').clone(false)).append(' ')
|
||||
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
|
||||
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
</script>
|
@@ -1,4 +1,7 @@
|
||||
@(issueLabels: List[model.Label])
|
||||
@if(issueLabels.isEmpty){
|
||||
<li><span class="muted small">None yet</span></li>
|
||||
}
|
||||
@issueLabels.map { label =>
|
||||
<li><span class="issue-label" style="background-color: #@label.color; color: #@label.fontColor;">@label.labelName</span></li>
|
||||
}
|
||||
|
@@ -1,51 +0,0 @@
|
||||
@(issue: model.Issue,
|
||||
issueLabels: List[model.Label],
|
||||
labels: List[model.Label],
|
||||
hasWritePermission: Boolean,
|
||||
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import view.helpers._
|
||||
<div style="margin-bottom: 8px;">
|
||||
<span class="strong">Labels</span>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
@helper.html.dropdown(right = true) {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="#" class="toggle-label" data-label-id="@label.labelId">
|
||||
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
|
||||
<span class="label" style="background-color: #@label.color;"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labellist(issueLabels)
|
||||
</ul>
|
||||
<script>
|
||||
$(function(){
|
||||
$('a.toggle-label').click(function(){
|
||||
var path, icon;
|
||||
var i = $(this).children('i');
|
||||
if(i.hasClass('icon-ok')){
|
||||
path = 'delete';
|
||||
icon = 'icon-white';
|
||||
} else {
|
||||
path = 'new';
|
||||
icon = 'icon-ok';
|
||||
}
|
||||
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
|
||||
{
|
||||
labelId : $(this).data('label-id')
|
||||
},
|
||||
function(data){
|
||||
i.removeClass().addClass(icon);
|
||||
$('ul.label-list').empty().html(data);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
@@ -1,45 +1,61 @@
|
||||
@(label: Option[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@defining((if(label.isEmpty) ("new", 190, 4) else ("edit", 180, 8))){ case (mode, width, margin) =>
|
||||
<div id="@(mode)LabelArea">
|
||||
<form method="POST" id="edit-label-form" validate="true" style="margin-bottom: 8px;"
|
||||
action="@url(repository)/issues/label/@{if(mode == "new") "new" else label.get.labelId + "/edit"}">
|
||||
<span id="error-@(mode)LabelName" class="error"></span>
|
||||
<input type="text" name="@(mode)LabelName" id="@(mode)LabelName" style="width: @(width)px; margin-left: @(margin)px; margin-bottom: 0px;" value="@label.map(_.labelName)"@if(mode == "new"){ placeholder="New label name"}/>
|
||||
<span id="error-@(mode)Color" class="error"></span>
|
||||
<div class="input-append color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" id="@(mode)Color" style="width: @(width)px; margin-bottom: 0px;">
|
||||
<input type="text" class="span3" name="@(mode)Color" value="#@label.map(_.color)" readonly style="width: @(width - 12)px; margin-left: @(margin)px;">
|
||||
@defining(label.map(_.labelId).getOrElse("new")){ labelId =>
|
||||
<div id="edit-label-area-@labelId">
|
||||
<form style="margin-bottom: 0px;">
|
||||
<input type="text" id="labelName-@labelId" style="width: 300px; margin-bottom: 0px;" value="@label.map(_.labelName)"@if(labelId == "new"){ placeholder="New label name"}/>
|
||||
<div id="label-color-@labelId" class="input-append color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; margin-bottom: 0px;">
|
||||
<input type="text" class="span3" id="labelColor-@labelId" value="#@label.map(_.color).getOrElse("888888")" readonly style="width: 100px;">
|
||||
<span class="add-on"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
|
||||
</div>
|
||||
<input type="submit" class="btn" style="margin-left: @(margin)px; margin-bottom: 0px;" value="@if(mode == "new"){Create} else {Save}"/>
|
||||
@if(mode == "edit"){
|
||||
<input type="hidden" name="editLabelId" value="@label.map(_.labelId)"/>
|
||||
}
|
||||
<script>
|
||||
$('div#label-color-@labelId').colorpicker();
|
||||
</script>
|
||||
<span class="pull-right">
|
||||
<span id="label-error-@labelId" class="error"></span>
|
||||
<input type="button" id="cancel-@labelId" class="btn label-edit-cancel" value="Cancel">
|
||||
<input type="button" id="submit-@labelId" class="btn btn-success" style="margin-bottom: 0px;" value="@(if(labelId == "new") "Create label" else "Save changes")"/>
|
||||
</span>
|
||||
</form>
|
||||
<script>
|
||||
$(function(){
|
||||
@if(mode == "new"){
|
||||
$('#newColor').colorpicker();
|
||||
</div>
|
||||
<script>
|
||||
$(function(){
|
||||
$('#submit-@labelId').click(function(e){
|
||||
$.post('@url(repository)/issues/labels/@{if(labelId == "new") "new" else labelId + "/edit"}', {
|
||||
'labelName' : $('#labelName-@labelId').val(),
|
||||
'labelColor': $('#labelColor-@labelId').val()
|
||||
}, function(data, status){
|
||||
$('div#edit-label-area-@labelId').remove();
|
||||
@if(labelId == "new"){
|
||||
$('#new-label-table').hide();
|
||||
// Insert row into the top of table
|
||||
$('#label-row-header').after(data);
|
||||
} else {
|
||||
// Replace table row
|
||||
$('#label-row-@labelId').after(data).remove();
|
||||
}
|
||||
}).fail(function(xhr, status, error){
|
||||
var errors = JSON.parse(xhr.responseText);
|
||||
if(errors.labelName){
|
||||
$('span#label-error-@labelId').text(errors.labelName);
|
||||
} else if(errors.labelColor){
|
||||
$('span#label-error-@labelId').text(errors.labelColor);
|
||||
} else {
|
||||
$('span#label-error-@labelId').text('error');
|
||||
}
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#cancel-@labelId').click(function(e){
|
||||
$('div#edit-label-area-@labelId').remove();
|
||||
@if(labelId == "new"){
|
||||
$('#new-label-table').hide();
|
||||
} else {
|
||||
$('#editColor').colorpicker();
|
||||
|
||||
$('#edit-label-form').submit(function(e){
|
||||
$.ajax($(this).attr('action'), {
|
||||
type: 'POST',
|
||||
data: $(this).serialize()
|
||||
})
|
||||
.done(function(data){
|
||||
$('#label-edit').parent().empty().html(data);
|
||||
})
|
||||
.fail(function(data, status){
|
||||
displayErrors($.parseJSON(data.responseText));
|
||||
});
|
||||
|
||||
return false;
|
||||
});
|
||||
$('#label-@labelId').show();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
});
|
||||
</script>
|
||||
}
|
||||
|
@@ -1,47 +0,0 @@
|
||||
@(labels: List[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<div id="label-edit">
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labels.map { label =>
|
||||
<li style="border: 1px solid white;">
|
||||
<a href="javascript:void(0);" class="label-edit-link" data-label-id="@label.labelId">
|
||||
<span class="count-right"><i class="icon-remove-circle"></i></span>
|
||||
<span style="background-color: #@label.color;" class="label-color"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<script>
|
||||
$(function(){
|
||||
$('i.icon-remove-circle').click(function(e){
|
||||
e.stopPropagation();
|
||||
if(confirm('Are you sure you want to delete this?')){
|
||||
$.get('@url(repository)/issues/label/' + $(this).parents('a').data('label-id') + '/delete',
|
||||
function(data){
|
||||
$('#label-edit').parent().empty().html(data);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$('a.label-edit-link').click(function(e){
|
||||
if($('input[name=editLabelId]').val() != $(this).data('label-id')){
|
||||
$('#editLabelArea').remove();
|
||||
var element = this;
|
||||
$.get('@url(repository)/issues/label/' + $(this).data('label-id') + '/edit',
|
||||
function(data){
|
||||
$(element).parent().append(data);
|
||||
$('div#label-edit li').css('border', '1px solid white');
|
||||
$(element).parent().css('border', '1px solid #eee');
|
||||
}
|
||||
);
|
||||
} else {
|
||||
$('#editLabelArea').remove();
|
||||
$('div#label-edit li').css('border', '1px solid white');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
36
src/main/twirl/issues/labels/label.scala.html
Normal file
@@ -0,0 +1,36 @@
|
||||
@(label: model.Label,
|
||||
counts: Map[String, Int],
|
||||
repository: service.RepositoryService.RepositoryInfo,
|
||||
hasWritePermission: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<tr id="label-row-@label.labelId">
|
||||
<td style="padding-top: 15px; padding-bottom: 15px;">
|
||||
<div class="milestone row-fluid" id="label-@label.labelId">
|
||||
<div class="span8">
|
||||
<div style="margin-top: 6px">
|
||||
<a href="@url(repository)/issues?labels=@urlEncode(label.labelName)" id="label-row-content-@label.labelId">
|
||||
<span style="background-color: #@label.color; color: #@label.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
|
||||
<img src="@assets/common/images/label_@(if(label.fontColor == "ffffff") "white" else "black").png" style="width: 12px;"/>
|
||||
@label.labelName
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="@if(hasWritePermission){span2} else {span4}">
|
||||
<div class="pull-right">
|
||||
<span class="muted">@counts.get(label.labelName).getOrElse(0) open issues</span>
|
||||
</div>
|
||||
</div>
|
||||
@if(hasWritePermission){
|
||||
<div class="span2">
|
||||
<div class="pull-right">
|
||||
<a href="javascript:void(0);" onclick="editLabel(@label.labelId)">Edit</a>
|
||||
|
||||
<a href="javascript:void(0);" onclick="deleteLabel(@label.labelId)">Delete</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
67
src/main/twirl/issues/labels/list.scala.html
Normal file
@@ -0,0 +1,67 @@
|
||||
@(labels: List[model.Label],
|
||||
counts: Map[String, Int],
|
||||
repository: service.RepositoryService.RepositoryInfo,
|
||||
hasWritePermission: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(s"Labels - ${repository.owner}/${repository.name}"){
|
||||
@html.menu("issues", repository){
|
||||
@issues.html.navigation("labels", hasWritePermission, repository)
|
||||
<br>
|
||||
<table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;">
|
||||
<tr><td></td></tr>
|
||||
</table>
|
||||
<table class="table table-bordered table-hover table-issues">
|
||||
<tr id="label-row-header">
|
||||
<th style="background-color: #eee;">
|
||||
<span class="small">@labels.size labels</span>
|
||||
</th>
|
||||
</tr>
|
||||
@labels.map { label =>
|
||||
@_root_.issues.labels.html.label(label, counts, repository, hasWritePermission)
|
||||
}
|
||||
@if(labels.isEmpty){
|
||||
<tr>
|
||||
<td style="padding: 20px; background-color: #eee; text-align: center;">
|
||||
No labels to show.
|
||||
@if(hasWritePermission){
|
||||
<a href="@url(repository)/issues/labels/new">Create a new label.</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$(function(){
|
||||
$('#new-label-button').click(function(e){
|
||||
if($('#edit-label-area-new').size() != 0){
|
||||
$('div#edit-label-area-new').remove();
|
||||
$('#new-label-table').hide();
|
||||
} else {
|
||||
$.get('@url(repository)/issues/labels/new',
|
||||
function(data){
|
||||
$('#new-label-table').show().find('tr td').append(data);
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function deleteLabel(labelId){
|
||||
if(confirm('Once you delete this label, there is no going back.\nAre you sure?')){
|
||||
$.post('@url(repository)/issues/labels/' + labelId + '/delete', function(){
|
||||
$('tr#label-row-' + labelId).remove();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function editLabel(labelId){
|
||||
$.get('@url(repository)/issues/labels/' + labelId + '/edit',
|
||||
function(data){
|
||||
$('#label-' + labelId).hide().parent().append(data);
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
@@ -1,143 +1,25 @@
|
||||
@(issues: List[(model.Issue, List[model.Label], Int)],
|
||||
@(target: String,
|
||||
issues: List[service.IssuesService.IssueInfo],
|
||||
page: Int,
|
||||
collaborators: List[String],
|
||||
milestones: List[model.Milestone],
|
||||
labels: List[model.Label],
|
||||
openCount: Int,
|
||||
closedCount: Int,
|
||||
allCount: Int,
|
||||
assignedCount: Option[Int],
|
||||
createdByCount: Option[Int],
|
||||
labelCounts: Map[String, Int],
|
||||
condition: service.IssuesService.IssueSearchCondition,
|
||||
filter: String,
|
||||
repository: service.RepositoryService.RepositoryInfo,
|
||||
hasWritePermission: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.menu("issues", repository){
|
||||
@tab("issues", false, repository)
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter == "all"){ class="active"}>
|
||||
<a href="@url(repository)/issues@condition.toURL">
|
||||
<span class="count-right">@allCount</span>
|
||||
Everyone's Issues
|
||||
</a>
|
||||
</li>
|
||||
@if(loginAccount.isDefined){
|
||||
<li@if(filter == "assigned"){ class="active"}>
|
||||
<a href="@url(repository)/issues/assigned/@loginAccount.map(_.userName)@condition.toURL">
|
||||
<span class="count-right">@assignedCount</span>
|
||||
Assigned to you
|
||||
</a>
|
||||
</li>
|
||||
<li@if(filter == "created_by"){ class="active"}>
|
||||
<a href="@url(repository)/issues/created_by/@loginAccount.map(_.userName)@condition.toURL">
|
||||
<span class="count-right">@createdByCount</span>
|
||||
Created by you
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<hr/>
|
||||
@if(condition.milestoneId.isEmpty){
|
||||
<span class="muted small">No milestone selected</span>
|
||||
} else {
|
||||
@if(condition.milestoneId.get.isEmpty){
|
||||
<span class="muted small">Issues with no milestone</span>
|
||||
} else {
|
||||
<span class="muted small">Milestone:</span> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map(_.title)
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown() {
|
||||
@if(condition.milestoneId.isDefined){
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = None).toURL">
|
||||
<i class="icon-remove-circle"></i> Clear milestone filter
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = Some(None)).toURL">
|
||||
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
|
||||
</a>
|
||||
</li>
|
||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
|
||||
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
|
||||
} else {
|
||||
<span class="muted">Due in @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){
|
||||
@milestones.find(_.milestoneId == condition.milestoneId.get.get).map { milestone =>
|
||||
<div style="margin-top: 4px;">
|
||||
@_root_.issues.milestones.html.progress(openCount + closedCount, closedCount, false)
|
||||
</div>
|
||||
<span class="muted small">@openCount open issues</span>
|
||||
@if(milestone.closedDate.isDefined){
|
||||
@milestone.closedDate.map { closedDate =>
|
||||
<span class="small">Closed in @date(closedDate)</span>
|
||||
}
|
||||
} else {
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert.png"/><span class="small milestone-alert">Due in @date(dueDate)</span>
|
||||
} else {
|
||||
<span class="small">Due in @date(dueDate)</span>
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
<hr/>
|
||||
<span class="strong">Labels</span>
|
||||
<div>
|
||||
<div id="label-list">
|
||||
<ul class="label-list nav nav-pills nav-stacked">
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL"
|
||||
@if(condition.labels.contains(label.labelName)){style="background-color: #@label.color; color: #@label.fontColor;"}>
|
||||
<span class="count-right">@labelCounts.getOrElse(label.labelName, 0)</span>
|
||||
<span style="background-color: #@label.color;" class="label-color"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@if(hasWritePermission){
|
||||
<hr/>
|
||||
<input type="button" class="btn btn-block" id="manageLabel" data-toggle="button" value="Manage Labels"/>
|
||||
<br/>
|
||||
<span class="strong">New label</span>
|
||||
@_root_.issues.labels.html.edit(None, repository)
|
||||
}
|
||||
</div>
|
||||
@***** show issue list *****@
|
||||
@listparts(issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
|
||||
</div>
|
||||
@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.menu(target, repository){
|
||||
@navigation(target, true, repository, Some(condition))
|
||||
@listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
|
||||
@if(hasWritePermission){
|
||||
<form id="batcheditForm" method="POST">
|
||||
<input type="hidden" name="value"/>
|
||||
<input type="hidden" name="checked"/>
|
||||
<input type="hidden" name="from" value="@target"/>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
@@ -145,20 +27,34 @@
|
||||
@if(hasWritePermission){
|
||||
<script>
|
||||
$(function(){
|
||||
$('#manageLabel').click(function(){
|
||||
if($(this).data('toggle-state')){
|
||||
location.href = '@url(repository)/issues';
|
||||
} else {
|
||||
$(this).data('toggle-state', 'on');
|
||||
$.get('@url(repository)/issues/label/edit', function(data){
|
||||
$('#label-list').parent().empty().html(data);
|
||||
});
|
||||
$('a.header-link').mouseover(function(e){
|
||||
var target = e.target;
|
||||
if(e.target.tagName != 'A'){
|
||||
target = e.target.parentElement;
|
||||
}
|
||||
$(target).children('strong' ).css('color', '#0088cc');
|
||||
$(target).children('img.header-icon-hover').css('display', 'inline');
|
||||
$(target).children('img.header-icon' ).css('display', 'none');
|
||||
});
|
||||
|
||||
$('a.header-link').mouseout(function(e){
|
||||
var target = e.target;
|
||||
if(e.target.tagName != 'A'){
|
||||
target = e.target.parentElement;
|
||||
}
|
||||
$(target).children('strong' ).css('color', 'black');
|
||||
$(target).children('img.header-icon-hover').css('display', 'none');
|
||||
$(target).children('img.header-icon' ).css('display', 'inline');
|
||||
});
|
||||
|
||||
$('.table-issues input[type=checkbox]').change(function(){
|
||||
$('.table-issues button').prop('disabled',
|
||||
!$('.table-issues input[type=checkbox]').filter(':checked').length);
|
||||
if($('.table-issues input[type=checkbox]').filter(':checked').length == 0){
|
||||
$('#table-issues-control').show();
|
||||
$('#table-issues-batchedit').hide();
|
||||
} else {
|
||||
$('#table-issues-control').hide();
|
||||
$('#table-issues-batchedit').show();
|
||||
}
|
||||
}).filter(':first').change();
|
||||
|
||||
var submitBatchEdit = function(action, value) {
|
||||
@@ -170,8 +66,8 @@ $(function(){
|
||||
form.submit();
|
||||
};
|
||||
|
||||
$('#state').click(function(){
|
||||
submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).text().toLowerCase());
|
||||
$('a.toggle-state').click(function(){
|
||||
submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).data('id'));
|
||||
});
|
||||
$('a.toggle-label').click(function(){
|
||||
submitBatchEdit('@url(repository)/issues/batchedit/label', $(this).data('id'));
|
||||
|
@@ -1,4 +1,5 @@
|
||||
@(issues: List[(model.Issue, List[model.Label], Int)],
|
||||
@(target: String,
|
||||
issues: List[service.IssuesService.IssueInfo],
|
||||
page: Int,
|
||||
openCount: Int,
|
||||
closedCount: Int,
|
||||
@@ -10,175 +11,208 @@
|
||||
hasWritePermission: Boolean = false)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
|
||||
<div class="span9">
|
||||
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
|
||||
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
|
||||
<i class="icon-remove-circle"></i> Clear milestone and label filters
|
||||
@import service.IssuesService.IssueInfo
|
||||
<br>
|
||||
@if(condition.nonEmpty){
|
||||
<div>
|
||||
<a href="@service.IssuesService.IssueSearchCondition().toURL" class="header-link">
|
||||
<img src="@assets/common/images/clear.png" class="header-icon"/>
|
||||
<img src="@assets/common/images/clear_hover.png" class="header-icon-hover" style="display: none;"/>
|
||||
<span class="strong">Clear current search query, filters, and sorts</span>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
<table class="table table-bordered table-hover table-issues">
|
||||
<tr>
|
||||
<th style="background-color: #eee;">
|
||||
<input type="checkbox"/>
|
||||
<span class="small">
|
||||
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
|
||||
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
|
||||
@openCount Open
|
||||
</a>
|
||||
<a class="button-link@if(condition.state == "closed"){ selected}" href="@condition.copy(state = "closed").toURL">
|
||||
<img src="@assets/common/images/status-closed@(if(condition.state == "closed"){"-active"}).png"/>
|
||||
@closedCount Closed
|
||||
</a>
|
||||
}
|
||||
@if(condition.repo.isDefined){
|
||||
<a href="@condition.copy(repo = None).toURL" id="clear-filter">
|
||||
<i class="icon-remove-circle"></i> Clear filter on @condition.repo
|
||||
</a>
|
||||
}
|
||||
<div class="pull-right">
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-small@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
|
||||
<a class="btn btn-small@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
|
||||
</div>
|
||||
@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
|
||||
){
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
<table class="table table-bordered table-hover table-issues">
|
||||
@if(issues.isEmpty){
|
||||
<tr>
|
||||
<td style="padding: 20px; background-color: #eee; text-align: center;">
|
||||
No issues to show.
|
||||
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
|
||||
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
|
||||
} else {
|
||||
@if(repository.isDefined){
|
||||
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
} else {
|
||||
@if(hasWritePermission){
|
||||
<tr>
|
||||
<td style="background-color: #eee;">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-mini strong" id="state">@{if(condition.state == "open") "Close" else "Reopen"}</button>
|
||||
</div>
|
||||
@helper.html.dropdown("Label") {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
|
||||
<i class="icon-white"></i>
|
||||
<span class="label" style="background-color: #@label.color;"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Assignee") {
|
||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Milestone") {
|
||||
<li><a href="javascript:void(0);" class="toggle-milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
|
||||
@milestones.map { milestone =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">
|
||||
<i class="icon-white"></i> @milestone.title
|
||||
<div class="small" style="padding-left: 20px;">
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
|
||||
} else {
|
||||
<span class="muted">Due in @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</span>
|
||||
<div class="pull-right" id="table-issues-control">
|
||||
@helper.html.dropdown("Author", flat = true) {
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
<a href="@condition.copy(author = (if(condition.author == Some(collaborator)) None else Some(collaborator))).toURL">
|
||||
@helper.html.checkicon(condition.author == Some(collaborator))
|
||||
@avatar(collaborator, 20) @collaborator
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Label", flat = true) {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL">
|
||||
@helper.html.checkicon(condition.labels.contains(label.labelName))
|
||||
<span style="background-color: #@label.color;" class="label-color"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Milestone", flat = true) {
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = Some(None)).toURL">
|
||||
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
|
||||
</a>
|
||||
</li>
|
||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||
<li>
|
||||
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
|
||||
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Assignee", flat = true) {
|
||||
@collaborators.map { collaborator =>
|
||||
<li>
|
||||
<a href="@condition.copy(assigned = Some(collaborator)).toURL">
|
||||
@helper.html.checkicon(condition.assigned == Some(collaborator))
|
||||
@avatar(collaborator, 20) @collaborator
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Sort", flat = true){
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@issues.map { case (issue, labels, commentCount) =>
|
||||
<tr>
|
||||
<td>
|
||||
@if(hasWritePermission){
|
||||
<label class="checkbox" style="cursor: default;">
|
||||
<input type="checkbox" value="@issue.issueId"/>
|
||||
}
|
||||
@if(issue.isPullRequest){
|
||||
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
|
||||
} else {
|
||||
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
|
||||
}
|
||||
@if(repository.isEmpty){
|
||||
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a> ・
|
||||
}
|
||||
@if(issue.isPullRequest){
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
||||
} else {
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
|
||||
}
|
||||
@labels.map { label =>
|
||||
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
|
||||
}
|
||||
<span class="pull-right muted">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@avatar(userName, 20, tooltip = true)
|
||||
}
|
||||
#@issue.issueId
|
||||
</span>
|
||||
<div class="small muted" style="margin-left: 20px;">
|
||||
Opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||
@if(commentCount > 0){
|
||||
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
|
||||
}
|
||||
</div>
|
||||
@if(hasWritePermission){
|
||||
</label>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<div class="pull-right">
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
|
||||
</div>
|
||||
</div>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right" id="table-issues-batchedit">
|
||||
@helper.html.dropdown("Mark as", flat = true) {
|
||||
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
|
||||
<li><a href="javascript:void(0);" class="toggle-state" data-id="close">Close</a></li>
|
||||
}
|
||||
@helper.html.dropdown("Label", flat = true) {
|
||||
@labels.map { label =>
|
||||
<li>
|
||||
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
|
||||
<i class="icon-white"></i>
|
||||
<span class="label" style="background-color: #@label.color;"> </span>
|
||||
@label.labelName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Milestone", flat = true) {
|
||||
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="">No milestone</a></li>
|
||||
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
|
||||
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">@milestone.title</a></li>
|
||||
}
|
||||
}
|
||||
@helper.html.dropdown("Assignee", flat = true) {
|
||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
|
||||
@collaborators.map { collaborator =>
|
||||
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</th>
|
||||
</tr>
|
||||
@if(issues.isEmpty){
|
||||
<tr>
|
||||
<td style="padding: 20px; background-color: #eee; text-align: center;">
|
||||
@if(target == "issues"){
|
||||
No issues to show.
|
||||
} else {
|
||||
No pull requests to show.
|
||||
}
|
||||
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
|
||||
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
|
||||
} else {
|
||||
@if(repository.isDefined){
|
||||
@if(target == "issues"){
|
||||
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
|
||||
} else {
|
||||
<a href="@url(repository.get)/compare">Create a new pull request.</a>
|
||||
}
|
||||
}
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
|
||||
<tr>
|
||||
<td style="padding-top: 15px; padding-bottom: 15px;">
|
||||
@if(hasWritePermission){
|
||||
<input type="checkbox" value="@issue.issueId"/>
|
||||
}
|
||||
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png" style="margin-right: 20px;"/>
|
||||
@if(repository.isEmpty){
|
||||
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a> ・
|
||||
}
|
||||
@if(target == "issues"){
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
|
||||
} else {
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
||||
}
|
||||
@labels.map { label =>
|
||||
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
|
||||
}
|
||||
<span class="pull-right small">
|
||||
@issue.assignedUserName.map { userName =>
|
||||
@avatar(userName, 20, tooltip = true)
|
||||
}
|
||||
@if(commentCount > 0){
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
|
||||
<img src="@assets/common/images/comment-active.png"> @commentCount
|
||||
</a>
|
||||
} else {
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
|
||||
<img src="@assets/common/images/comment.png"> @commentCount
|
||||
</a>
|
||||
}
|
||||
</span>
|
||||
<div class="small muted" style="margin-left: 40px; margin-top: 5px;">
|
||||
#@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username")
|
||||
@milestone.map { milestone =>
|
||||
<span style="margin: 20px;"><a href="@condition.copy(milestoneId = Some(Some(1))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<div class="pull-right">
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
|
||||
</div>
|
||||
|
||||
|
@@ -2,12 +2,18 @@
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
|
||||
@html.menu("milestones", repository){
|
||||
@issues.html.tab("milestones", false, repository)
|
||||
@html.menu("issues", repository){
|
||||
@if(milestone.isEmpty){
|
||||
<h4>New milestone</h4>
|
||||
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
|
||||
} else {
|
||||
@issues.html.navigation("milestones", false, repository)
|
||||
<br><br>
|
||||
}
|
||||
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>
|
||||
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
|
||||
<fieldset>
|
||||
<label for="title"><string>Title</string></label>
|
||||
<input type="text" id="title" name="title" style="width: 400px;" value="@milestone.map(_.title)"/>
|
||||
<input type="text" id="title" name="title" style="width: 500px;" value="@milestone.map(_.title)" placeholder="Title"/>
|
||||
<span id="error-title" class="error"></span>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
|
@@ -6,94 +6,94 @@
|
||||
@import view.helpers._
|
||||
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){
|
||||
@html.menu("issues", repository){
|
||||
@issues.html.tab("milestones", false, repository)
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(state == "open"){ class="active"}>
|
||||
<a href="?state=open">
|
||||
<span class="count-right">@milestones.filter(_._1.closedDate.isEmpty).size</span>
|
||||
Open Milestones
|
||||
@issues.html.navigation("milestones", hasWritePermission, repository)
|
||||
<br>
|
||||
<table class="table table-bordered table-hover table-issues">
|
||||
<tr>
|
||||
<th style="background-color: #eee;">
|
||||
<span class="small">
|
||||
<a class="button-link@if(state == "open"){ selected}" href="?state=open">
|
||||
<img src="@assets/common/images/milestone@(if(state == "open"){"-active"}).png"/>
|
||||
@milestones.filter(_._1.closedDate.isEmpty).size Open
|
||||
</a>
|
||||
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
|
||||
<img src="@assets/common/images/milestone@(if(state == "closed"){"-active"}).png"/>
|
||||
@milestones.filter(_._1.closedDate.isDefined).size Closed
|
||||
</a>
|
||||
</li>
|
||||
<li@if(state == "closed"){ class="active"}>
|
||||
<a href="?state=closed">
|
||||
<span class="count-right">@milestones.filter(_._1.closedDate.isDefined).size</span>
|
||||
Closed Milestones
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@if(hasWritePermission){
|
||||
<hr>
|
||||
<a href="@url(repository)/issues/milestones/new" class="btn btn-block">Create a new milestone</a>
|
||||
}
|
||||
</div>
|
||||
<div class="span9">
|
||||
<table class="table table-bordered table-hover">
|
||||
@defining(milestones.filter { case (milestone, _, _) =>
|
||||
milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open")
|
||||
}){ milestones =>
|
||||
@milestones.map { case (milestone, openCount, closedCount) =>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="milestone row-fluid">
|
||||
<div class="span4">
|
||||
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a><br>
|
||||
@if(milestone.closedDate.isDefined){
|
||||
<span class="muted">Closed @datetime(milestone.closedDate.get)</span>
|
||||
} else {
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due in @date(dueDate)</span>
|
||||
} else {
|
||||
<span class="muted">Due in @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
@defining(milestones.filter { case (milestone, _, _) =>
|
||||
milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open")
|
||||
}){ milestones =>
|
||||
@milestones.map { case (milestone, openCount, closedCount) =>
|
||||
<tr>
|
||||
<td style="padding-top: 15px; padding-bottom: 15px;">
|
||||
<div class="milestone row-fluid">
|
||||
<div class="span4">
|
||||
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a>
|
||||
<div style="margin-top: 6px">
|
||||
@if(milestone.closedDate.isDefined){
|
||||
<span class="muted">Closed @helper.html.datetimeago(milestone.closedDate.get)</span>
|
||||
} else {
|
||||
@milestone.dueDate.map { dueDate =>
|
||||
@if(isPast(dueDate)){
|
||||
<img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due by @date(dueDate)</span>
|
||||
} else {
|
||||
<span class="muted">Due by @date(dueDate)</span>
|
||||
}
|
||||
}.getOrElse {
|
||||
<span class="muted">No due date</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="span8">
|
||||
@progress(openCount + closedCount, closedCount)
|
||||
<div>
|
||||
<div>
|
||||
@if(closedCount == 0){
|
||||
0%
|
||||
} else {
|
||||
@((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%
|
||||
} <span class="muted">complete</span>
|
||||
@openCount <span class="muted">open</span>
|
||||
@closedCount <span class="muted">closed</span>
|
||||
</div>
|
||||
<div class="milestone-menu">
|
||||
@if(hasWritePermission){
|
||||
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/edit">Edit</a>
|
||||
@if(milestone.closedDate.isDefined){
|
||||
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/open">Open</a>
|
||||
} else {
|
||||
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/close">Close</a>
|
||||
}
|
||||
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/delete" class="delete">Delete</a>
|
||||
}
|
||||
</div>
|
||||
<div class="span8">
|
||||
<div class="milestone-menu">
|
||||
<div class="pull-right">
|
||||
@if(hasWritePermission){
|
||||
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/edit">Edit
|
||||
@if(milestone.closedDate.isDefined){
|
||||
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/open">Open</a>
|
||||
} else {
|
||||
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/close">Close</a>
|
||||
}
|
||||
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/delete" class="delete">Delete</a>
|
||||
}
|
||||
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open">Browse issues</a>
|
||||
</div>
|
||||
<span class="muted">@closedCount closed - @openCount open</span>
|
||||
</div>
|
||||
@progress(openCount + closedCount, closedCount, true)
|
||||
</div>
|
||||
</div>
|
||||
@if(milestone.description.isDefined){
|
||||
<div class="milestone-description">
|
||||
@markdown(milestone.description.get, repository, false, false)
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</div>
|
||||
</div>
|
||||
@if(milestone.description.isDefined){
|
||||
<div class="milestone-description">
|
||||
@markdown(milestone.description.get, repository, false, false)
|
||||
</div>
|
||||
}
|
||||
@if(milestones.isEmpty){
|
||||
<tr>
|
||||
<td style="padding: 20px; background-color: #eee; text-align: center;">
|
||||
No milestones to show.
|
||||
@if(hasWritePermission){
|
||||
<a href="@url(repository)/issues/milestones/new">Create a new milestone.</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if(milestones.isEmpty){
|
||||
<tr>
|
||||
<td style="padding: 20px; background-color: #eee; text-align: center;">
|
||||
No milestones to show.
|
||||
@if(hasWritePermission){
|
||||
<a href="@url(repository)/issues/milestones/new">Create a new milestone.</a>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</table>
|
||||
}
|
||||
}
|
||||
<script>
|
||||
|
@@ -1,15 +1,6 @@
|
||||
@(total: Int, progress: Int, showPercentage: Boolean)
|
||||
@(total: Int, progress: Int)
|
||||
<div class="milestone-progress">
|
||||
@if(progress > 0){
|
||||
<span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span>
|
||||
}
|
||||
@if(showPercentage){
|
||||
<span class="milestone-percentage">
|
||||
@if(progress == 0){
|
||||
0%
|
||||
} else {
|
||||
@((progress.toDouble / total.toDouble * 100).toInt)%
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
58
src/main/twirl/issues/navigation.scala.html
Normal file
@@ -0,0 +1,58 @@
|
||||
@(active: String,
|
||||
newButton: Boolean,
|
||||
repository: service.RepositoryService.RepositoryInfo,
|
||||
condition: Option[service.IssuesService.IssueSearchCondition] = None)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<ul class="nav nav-pills-group pull-left fill-width">
|
||||
<li class="@if(active == "issues" ){active} first"><a href="@url(repository)/issues">Issues</a></li>
|
||||
<li class="@if(active == "pulls" ){active}"><a href="@url(repository)/pulls">Pull requests</a></li>
|
||||
<li class="@if(active == "labels" ){active}"><a href="@url(repository)/issues/labels">Labels</a></li>
|
||||
<li class="@if(active == "milestones"){active} last"><a href="@url(repository)/issues/milestones">Milestones</a></li>
|
||||
<li class="pull-right">
|
||||
<form method="GET" id="search-filter-form" style="margin-bottom: 0px;">
|
||||
@condition.map { condition =>
|
||||
@if(loginAccount.isDefined){
|
||||
<div class="input-prepend" style="margin-bottom: 0px;">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn dropdown-toggle" data-toggle="dropdown" style="height: 34px;">
|
||||
Filter
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="?q=is:open">Open issues and pull requests</a></li>
|
||||
<li><a href="?q=is:open+is:issue+author:@urlEncode(loginAccount.get.userName)">Your issues</a></li>
|
||||
<li><a href="?q=is:open+is:pr+author:@urlEncode(loginAccount.get.userName)">Your pull requests</a></li>
|
||||
<li><a href="?q=is:open+assignee:@urlEncode(loginAccount.get.userName)">Everything assigned to you</a></li>
|
||||
@*
|
||||
<li><a href="?q=is:open+mentions:@urlEncode(loginAccount.get.userName)">Everything mentioning you</a></li>
|
||||
*@
|
||||
</ul>
|
||||
</div>
|
||||
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
|
||||
</div>
|
||||
} else {
|
||||
<input type="text" id="search-filter-box" class="input-xlarge" name="q" style="height: 24px;" value="is:@{if(active == "issues") "issue" else "pr"} @condition.toFilterString"/>
|
||||
}
|
||||
}
|
||||
@if(loginAccount.isDefined){
|
||||
<div class="btn-group">
|
||||
@if(newButton){
|
||||
@if(active == "issues"){
|
||||
<a class="btn btn-success" href="@url(repository)/issues/new" style="height: 24px;">New issue</a>
|
||||
}
|
||||
@if(active == "pulls"){
|
||||
<a class="btn btn-success" href="@url(repository)/compare" style="height: 24px;">New pull request</a>
|
||||
}
|
||||
@if(active == "labels"){
|
||||
<a class="btn btn-success" href="javascript:void(0);" id="new-label-button" style="height: 24px;">New label</a>
|
||||
}
|
||||
@if(active == "milestones"){
|
||||
<a class="btn btn-success" href="@url(repository)/issues/milestones/new" style="height: 24px;">New milestone</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
@@ -1,18 +0,0 @@
|
||||
@(active: String, create: Boolean, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<ul class="nav nav-tabs pull-left fill-width">
|
||||
<li@if(active == "issues"){ class="active"}><a href="@url(repository)/issues">Browse Issues</a></li>
|
||||
<li@if(active == "milestones"){ class="active"}><a href="@url(repository)/issues/milestones">Milestones</a></li>
|
||||
@if(loginAccount.isDefined){
|
||||
<li class="pull-right">
|
||||
<div class="btn-group">
|
||||
@if(create){
|
||||
<a class="btn btn-small btn-success" href="#" disabled="disabled">New Issue</a>
|
||||
} else {
|
||||
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New Issue</a>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
@@ -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 @@
|
||||
}
|
||||
|
||||
<div class="container">
|
||||
@helper.html.information(info)
|
||||
@helper.html.error(error)
|
||||
@if(repository.commitCount > 0){
|
||||
<div class="pull-right">
|
||||
<div class="input-prepend">
|
||||
|
@@ -45,7 +45,7 @@
|
||||
</div>
|
||||
@if(commits.nonEmpty && hasWritePermission){
|
||||
<div style="margin-bottom: 10px;" id="create-pull-request">
|
||||
<a href="#" class="btn" id="show-form">Click to create a pull request for this comparison</a>
|
||||
<a href="#" class="btn btn-success" id="show-form">Create pull request</a>
|
||||
</div>
|
||||
<div id="pull-request-form" class="box" style="display: none;">
|
||||
<div class="box-content">
|
||||
@@ -58,7 +58,7 @@
|
||||
<div style="width: 600px; border-right: 1px solid #d4d4d4;">
|
||||
<span class="error" id="error-title"></span>
|
||||
<input type="text" name="title" style="width: 580px" placeholder="Title"/>
|
||||
@helper.html.preview(repository, "", false, true, "width: 580px; height: 200px;")
|
||||
@helper.html.preview(repository, "", false, true, true, hasWritePermission, "width: 580px; height: 200px;")
|
||||
<input type="hidden" name="targetUserName" value="@originRepository.owner"/>
|
||||
<input type="hidden" name="targetBranch" value="@originId"/>
|
||||
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>
|
||||
|
@@ -11,7 +11,6 @@
|
||||
@import view.helpers._
|
||||
<div class="row-fluid">
|
||||
<div class="span10">
|
||||
@issues.html.issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
|
||||
@issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq))
|
||||
@defining(comments.exists(_.action == "merge")){ merged =>
|
||||
@if(hasWritePermission && !issue.closed){
|
||||
@@ -49,25 +48,12 @@
|
||||
<span class="small muted">You're all set-the <span class="label label-info monospace">@pullreq.requestBranch</span> branch can be safely deleted.</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@issues.html.commentform(issue, !merged, hasWritePermission, repository)
|
||||
</div>
|
||||
<div class="span2">
|
||||
@if(issue.closed) {
|
||||
@if(merged){
|
||||
<span class="label label-info issue-status">Merged</span>
|
||||
} else {
|
||||
<span class="label label-important issue-status">Closed</span>
|
||||
}
|
||||
} else {
|
||||
<span class="label label-success issue-status">Open</span>
|
||||
@issues.html.commentform(issue, !merged, hasWritePermission, repository)
|
||||
}
|
||||
<div class="small" style="text-align: center;">
|
||||
<span class="strong">@comments.size</span> @plural(comments.size, "comment")
|
||||
</div>
|
||||
}
|
||||
<hr/>
|
||||
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository)
|
||||
</div>
|
||||
<div class="span2">
|
||||
@issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
@@ -1,51 +0,0 @@
|
||||
@(issues: List[(model.Issue, List[model.Label], Int)],
|
||||
counts: List[service.PullRequestService.PullRequestCount],
|
||||
filter: Option[String],
|
||||
page: Int,
|
||||
openCount: Int,
|
||||
closedCount: Int,
|
||||
allCount: Int,
|
||||
condition: service.IssuesService.IssueSearchCondition,
|
||||
repository: service.RepositoryService.RepositoryInfo,
|
||||
hasWritePermission: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main(s"Pull Requests - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.menu("pulls", repository){
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li@if(filter.isEmpty){ class="active"}>
|
||||
<a href="@url(repository)/pulls">
|
||||
<span class="count-right">@allCount</span>
|
||||
All Requests
|
||||
</a>
|
||||
</li>
|
||||
@if(loginAccount.isDefined){
|
||||
<li@if(filter.map(_ == loginAccount.get.userName).getOrElse(false)){ class="active"}>
|
||||
<a href="@url(repository)/pulls/@loginAccount.map(_.userName)">
|
||||
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
|
||||
Yours
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
<hr>
|
||||
<ul class="nav nav-pills nav-stacked small">
|
||||
@counts.map { user =>
|
||||
@if(loginAccount.isEmpty || loginAccount.get.userName != user.userName){
|
||||
<li@if(filter.map(_ == user.userName).getOrElse(false)){ class="active"}>
|
||||
<a href="@url(repository)/pulls/@user.userName">
|
||||
<span class="count-right">@user.count</span>
|
||||
@user.userName
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
@listparts(issues, page, openCount, closedCount, condition, Some(repository), hasWritePermission)
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
@@ -1,100 +0,0 @@
|
||||
@(issues: List[(model.Issue, List[model.Label], Int)],
|
||||
page: Int,
|
||||
openCount: Int,
|
||||
closedCount: Int,
|
||||
condition: service.IssuesService.IssueSearchCondition,
|
||||
repository: Option[service.RepositoryService.RepositoryInfo],
|
||||
hasWritePermission: Boolean)(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
<div class="span9">
|
||||
@repository.map { repository =>
|
||||
@if(hasWritePermission){
|
||||
<div class="pull-right">
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 7, condition.toURL)
|
||||
<a href="@url(repository)/compare" class="btn btn-small btn-success">New pull request</a>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-small@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
|
||||
<a class="btn btn-small@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
|
||||
</div>
|
||||
@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
|
||||
){
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="desc").toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
|
||||
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
<table class="table table-bordered table-hover table-issues">
|
||||
@if(issues.isEmpty){
|
||||
<tr>
|
||||
<td style="padding: 20px; background-color: #eee; text-align: center;">
|
||||
No pull requests to show.
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@issues.map { case (issue, labels, commentCount) =>
|
||||
<tr>
|
||||
<td>
|
||||
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
|
||||
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
|
||||
<span class="pull-right muted">#@issue.issueId</span>
|
||||
<div style="margin-left: 20px;">
|
||||
@issue.content.map { content =>
|
||||
@cut(content, 90)
|
||||
}.getOrElse {
|
||||
<span class="muted">No description available</span>
|
||||
}
|
||||
</div>
|
||||
<div class="small muted" style="margin-left: 20px;">
|
||||
@avatarLink(issue.openedUserName, 20) by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
|
||||
@if(commentCount > 0){
|
||||
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<div class="pull-right">
|
||||
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL)
|
||||
</div>
|
||||
</div>
|
@@ -6,4 +6,4 @@
|
||||
<h4 style="color: #468847;">Able to merge</h4>
|
||||
<p>These branches can be automatically merged.</p>
|
||||
}
|
||||
<input type="submit" class="btn btn-success btn-block" value="Send pull request"/>
|
||||
<input type="submit" class="btn btn-success btn-block" value="Create pull request"/>
|
||||
|
@@ -36,7 +36,7 @@
|
||||
</p>
|
||||
}
|
||||
@helper.html.copy("repository-url-copy", requestRepositoryUrl){
|
||||
<input type="text" value="@requestRepositoryUrl" id="repository-url" readonly>
|
||||
<input type="text" style="width: 500px;" value="@requestRepositoryUrl" id="repository-url" readonly>
|
||||
}
|
||||
<div>
|
||||
<p>
|
||||
|
@@ -14,24 +14,50 @@
|
||||
@html.main(s"${issue.title} - Pull Request #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
|
||||
@html.menu("pulls", repository){
|
||||
@defining(dayByDayCommits.flatten){ commits =>
|
||||
<div class="pullreq-info">
|
||||
@if(issue.closed) {
|
||||
@comments.find(_.action == "merge").map{ comment =>
|
||||
<span class="label label-info">Merged</span>
|
||||
<div>
|
||||
<div class="show-title pull-right">
|
||||
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
|
||||
<a class="btn btn-small" href="#" id="edit">Edit</a>
|
||||
}
|
||||
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
|
||||
</div>
|
||||
<div class="edit-title pull-right" style="display: none;">
|
||||
<a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
|
||||
</div>
|
||||
<h1>
|
||||
<span class="show-title">
|
||||
<span id="show-title">@issue.title</span>
|
||||
<span class="muted">#@issue.issueId</span>
|
||||
</span>
|
||||
<span class="edit-title" style="display: none;">
|
||||
<span id="error-edit-title" class="error"></span>
|
||||
<input type="text" style="width: 700px;" id="edit-title" value="@issue.title"/>
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
@if(issue.closed) {
|
||||
@comments.find(_.action == "merge").map{ comment =>
|
||||
<span class="label label-info issue-status">Merged</span>
|
||||
<span class="muted">
|
||||
@user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||
at @datetime(comment.registeredDate)
|
||||
}.getOrElse {
|
||||
<span class="label label-important">Closed</span>
|
||||
@helper.html.datetimeago(comment.registeredDate)
|
||||
</span>
|
||||
}.getOrElse {
|
||||
<span class="label label-important issue-status">Closed</span>
|
||||
<span class="muted">
|
||||
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||
}
|
||||
} else {
|
||||
<span class="label label-success">Open</span>
|
||||
</span>
|
||||
}
|
||||
} else {
|
||||
<span class="label label-success issue-status">Open</span>
|
||||
<span class="muted">
|
||||
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")
|
||||
into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
|
||||
}
|
||||
</div>
|
||||
</span>
|
||||
}
|
||||
<br/><br/>
|
||||
<ul class="nav nav-tabs fill-width pull-left" id="pullreq-tab">
|
||||
<li class="active"><a href="#conversation">Conversation <span class="badge">@comments.size</span></a></li>
|
||||
<li><a href="#commits">Commits <span class="badge">@commits.size</span></a></li>
|
||||
@@ -52,8 +78,41 @@
|
||||
}
|
||||
}
|
||||
<script>
|
||||
$('#pullreq-tab a').click(function (e) {
|
||||
e.preventDefault();
|
||||
$(this).tab('show');
|
||||
$(function(){
|
||||
$('#pullreq-tab a').click(function (e) {
|
||||
e.preventDefault();
|
||||
$(this).tab('show');
|
||||
});
|
||||
|
||||
$('#edit').click(function(){
|
||||
$('.edit-title').show();
|
||||
$('.show-title').hide();
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#update').click(function(){
|
||||
$(this).attr('disabled', 'disabled');
|
||||
$.ajax({
|
||||
url: '@url(repository)/issues/edit_title/@issue.issueId',
|
||||
type: 'POST',
|
||||
data: {
|
||||
title : $('#edit-title').val()
|
||||
}
|
||||
}).done(function(data){
|
||||
$('#show-title').empty().text(data.title);
|
||||
$('#cancel').click();
|
||||
$(this).removeAttr('disabled');
|
||||
}).fail(function(req){
|
||||
$(this).removeAttr('disabled');
|
||||
$('#error-edit-title').text($.parseJSON(req.responseText).title);
|
||||
});
|
||||
return false;
|
||||
});
|
||||
|
||||
$('#cancel').click(function(){
|
||||
$('.edit-title').hide();
|
||||
$('.show-title').show();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
</script>
|
@@ -9,10 +9,10 @@
|
||||
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
|
||||
@html.menu("code", repository){
|
||||
<div class="head">
|
||||
@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 =>
|
||||
<li><a href="@url(repository)/blob/@encodeRefName(x)/@pathList.mkString("/")">@helper.html.checkicon(x == branch) @x</a></li>
|
||||
@@ -34,7 +34,7 @@
|
||||
<div class="pull-left">
|
||||
@avatar(latestCommit, 20)
|
||||
@user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong")
|
||||
<span class="muted">@datetime(latestCommit.commitTime)</span>
|
||||
<span class="muted">@helper.html.datetimeago(latestCommit.commitTime)</span>
|
||||
<a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
|
||||
</div>
|
||||
<div class="btn-group pull-right">
|
||||
|
@@ -22,7 +22,7 @@
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@datetime(latestUpdateDate)
|
||||
@helper.html.datetimeago(latestUpdateDate, false)
|
||||
</td>
|
||||
<td>
|
||||
@if(repository.repository.defaultBranch == branchName){
|
||||
|
@@ -68,13 +68,13 @@
|
||||
<div class="author">
|
||||
@avatar(commit, 20)
|
||||
<span>@user(commit.authorName, commit.authorEmailAddress, "username strong")</span>
|
||||
<span class="muted">authored on @datetime(commit.authorTime)</span>
|
||||
<span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
|
||||
</div>
|
||||
@if(commit.isDifferentFromAuthor) {
|
||||
<div class="committer">
|
||||
<span class="icon-arrow-right"></span>
|
||||
<span>@user(commit.committerName, commit.committerEmailAddress, "username strong")</span>
|
||||
<span class="muted"> committed on @datetime(commit.commitTime)</span>
|
||||
<span class="muted"> committed @helper.html.datetimeago(commit.commitTime)</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@@ -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){
|
||||
<div class="head">
|
||||
@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 =>
|
||||
<li><a href="@url(repository)/commits/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li>
|
||||
@@ -58,11 +59,11 @@
|
||||
}
|
||||
<div class="small">
|
||||
@user(commit.authorName, commit.authorEmailAddress, "username")
|
||||
<span class="muted">authored @datetime(commit.authorTime)</span>
|
||||
<span class="muted">authored @helper.html.datetimeago(commit.authorTime)</span>
|
||||
@if(commit.isDifferentFromAuthor) {
|
||||
<span class="icon-arrow-right" style="margin-top : -2px ;"></span>
|
||||
@user(commit.committerName, commit.committerEmailAddress, "username")
|
||||
<span class="muted">committed @datetime(commit.authorTime)</span>
|
||||
<span class="muted">committed @helper.html.datetimeago(commit.authorTime)</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -56,6 +56,6 @@
|
||||
<link href="@assets/vendors/jsdifflib/diffview.css" type="text/css" rel="stylesheet" />
|
||||
<script>
|
||||
$(function(){
|
||||
diffUsingJS('oldText', 'newText', 'diffText');
|
||||
diffUsingJS('oldText', 'newText', 'diffText', 1);
|
||||
});
|
||||
</script>
|
@@ -139,7 +139,7 @@ $(function(){
|
||||
.append($('<div id="diffText">'))
|
||||
.append($('<textarea id="newText" style="display: none;">').html(editor.getValue()))
|
||||
.append($('<textarea id="oldText" style="display: none;">').html($('#initial').val()));
|
||||
diffUsingJS('oldText', 'newText', 'diffText');
|
||||
diffUsingJS('oldText', 'newText', 'diffText', 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -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){
|
||||
<div class="head">
|
||||
@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 =>
|
||||
<li><a href="@url(repository)/tree/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li>
|
||||
@@ -47,13 +49,13 @@
|
||||
<div class="author">
|
||||
@avatar(latestCommit, 20)
|
||||
<span>@user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong")</span>
|
||||
<span class="muted"> authored on @datetime(latestCommit.authorTime)</span>
|
||||
<span class="muted"> authored @helper.html.datetimeago(latestCommit.authorTime)</span>
|
||||
</div>
|
||||
@if(latestCommit.isDifferentFromAuthor) {
|
||||
<div class="committer">
|
||||
<span class="icon-arrow-right"></span>
|
||||
<span>@user(latestCommit.committerName, latestCommit.committerEmailAddress, "username strong")</span>
|
||||
<span class="muted"> committed on @datetime(latestCommit.commitTime)</span>
|
||||
<span class="muted"> committed @helper.html.datetimeago(latestCommit.commitTime)</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -84,9 +86,19 @@
|
||||
<td>
|
||||
@if(file.isDirectory){
|
||||
@if(file.linkUrl.isDefined){
|
||||
<a href="@file.linkUrl">@file.name</a>
|
||||
<a href="@file.linkUrl">
|
||||
<span class="simplified-path">@file.name.split("/").toList.init match {
|
||||
case Nil => {}
|
||||
case list => {@list.mkString("", "/", "/")}
|
||||
}</span>@file.name.split("/").toList.last
|
||||
</a>
|
||||
} else {
|
||||
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
|
||||
<a href="@url(repository)/tree@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">
|
||||
<span class="simplified-path">@file.name.split("/").toList.init match {
|
||||
case Nil => {}
|
||||
case list => {@list.mkString("", "/", "/")}
|
||||
}</span>@file.name.split("/").toList.last
|
||||
</a>
|
||||
}
|
||||
} else {
|
||||
<a href="@url(repository)/blob@{(encodeRefName(branch) :: pathList).mkString("/", "/", "/")}@file.name">@file.name</a>
|
||||
@@ -96,7 +108,7 @@
|
||||
<a href="@url(repository)/commit/@file.commitId" class="commit-message">@link(file.message, repository)</a>
|
||||
[@user(file.author, file.mailAddress)]
|
||||
</td>
|
||||
<td style="text-align: right;">@datetime(file.time)</td>
|
||||
<td style="text-align: right;">@helper.html.datetimeago(file.time, false)</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
|
@@ -14,7 +14,7 @@
|
||||
@repository.tags.reverse.map { tag =>
|
||||
<tr>
|
||||
<td><a href="@url(repository)/tree/@encodeRefName(tag.name)">@tag.name</a></td>
|
||||
<td>@datetime(tag.time)</td>
|
||||
<td>@helper.html.datetimeago(tag.time, false)</td>
|
||||
<td class="monospace"><a href="@url(repository)/commit/@tag.id">@tag.id.substring(0, 10)</a></td>
|
||||
<td>
|
||||
<a href="@url(repository)/archive/@{encodeRefName(tag.name)}.zip">ZIP</a>
|
||||
|
@@ -16,7 +16,7 @@
|
||||
@files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
|
||||
<div>
|
||||
<h5><a href="@url(repository)/blob/@repository.repository.defaultBranch/@file.path">@file.path</a></h5>
|
||||
<div class="small muted">Latest commit at @datetime(file.lastModified)</div>
|
||||
<div class="small muted">Last commited @helper.html.datetimeago(file.lastModified)</div>
|
||||
<pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre>
|
||||
</div>
|
||||
}
|
||||
|
@@ -22,7 +22,7 @@
|
||||
}
|
||||
<div class="small muted">
|
||||
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a>
|
||||
at @datetime(issue.registeredDate)
|
||||
@helper.html.datetimeago(issue.registeredDate)
|
||||
@if(issue.commentCount > 0){
|
||||
<i class="icon-comment"></i><span class="strong">@issue.commentCount</span> @plural(issue.commentCount, "comment")
|
||||
}
|
||||
|
@@ -1,4 +1,7 @@
|
||||
@(webHooks: List[model.WebHook], repository: service.RepositoryService.RepositoryInfo, info: Option[Any])(implicit context: app.Context)
|
||||
@(webHooks: List[model.WebHook],
|
||||
enteredUrl: Option[Any],
|
||||
repository: service.RepositoryService.RepositoryInfo,
|
||||
info: Option[Any])(implicit context: app.Context)
|
||||
@import context._
|
||||
@import view.helpers._
|
||||
@html.main("Settings", Some(repository)){
|
||||
@@ -11,13 +14,13 @@
|
||||
<li>@webHook.url <a href="@url(repository)/settings/hooks/delete?url=@urlEncode(webHook.url)" class="remove">(remove)</a></li>
|
||||
}
|
||||
</ul>
|
||||
<form method="POST" action="@url(repository)/settings/hooks/add" validate="true">
|
||||
<form method="POST" validate="true">
|
||||
<div>
|
||||
<span class="error" id="error-url"></span>
|
||||
</div>
|
||||
<input type="text" name="url" id="url" style="width: 300px; margin-bottom: 0px;"/>
|
||||
<input type="submit" class="btn" value="Add"/>
|
||||
<a href="@url(repository)/settings/hooks/test" class="btn">Test Hook</a>
|
||||
<input type="text" name="url" id="url" value="@enteredUrl" style="width: 300px; margin-bottom: 0px;"/>
|
||||
<input type="submit" class="btn" formaction="@url(repository)/settings/hooks/add" value="Add"/>
|
||||
<input type="submit" class="btn" formaction="@url(repository)/settings/hooks/test" value="Test Hook"/>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
@@ -10,19 +10,19 @@
|
||||
<h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<div class="btn-group">
|
||||
@if(page.isDefined){
|
||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
|
||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_delete" id="delete">Delete Page</a>
|
||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
|
||||
}
|
||||
<div>
|
||||
@if(page.isDefined){
|
||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_delete" id="delete">Delete Page</a>
|
||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
|
||||
}
|
||||
<a class="btn btn-small btn-success" href="@url(repository)/wiki/_new">New Page</a>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<form action="@url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true">
|
||||
<span id="error-pageName" class="error"></span>
|
||||
<input type="text" name="pageName" value="@pageName" style="width: 850px; font-weight: bold;" placeholder="Input a page name."/>
|
||||
@helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, "width: 850px; height: 400px;", "")
|
||||
@helper.html.preview(repository, page.map(_.content).getOrElse(""), true, false, false, false, "width: 850px; height: 400px;", "")
|
||||
<input type="text" name="message" value="" style="width: 850px;" placeholder="Write a small message here explaining this change. (Optional)"/>
|
||||
<input type="hidden" name="currentPageName" value="@pageName"/>
|
||||
<input type="hidden" name="id" value="@page.map(_.id)"/>
|
||||
@@ -36,4 +36,4 @@ $(function(){
|
||||
return confirm('Are you sure you want to delete this page?');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
@@ -16,33 +16,39 @@
|
||||
</h1>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<div class="btn-group">
|
||||
<div>
|
||||
@if(pageName.isEmpty){
|
||||
@if(loginAccount.isDefined){
|
||||
<a class="btn btn-small" href="@url(repository)/wiki/_new">New Page</a>
|
||||
}
|
||||
} else {
|
||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
|
||||
@if(loginAccount.isDefined){
|
||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a>
|
||||
<a class="btn btn-small btn-success" href="@url(repository)/wiki/_new">New Page</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<table class="table table-bordered fill-width pull-left">
|
||||
<tr>
|
||||
<th colspan="3">
|
||||
<div class="pull-left" style="padding-top: 4px;">Revisions</div>
|
||||
<div class="pull-right">
|
||||
<input type="button" id="compare" value="Compare Revisions" class="btn btn-mini"/>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
@commits.map { commit =>
|
||||
<tr>
|
||||
<td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td>
|
||||
<td>@avatar(commit, 20) @user(commit.authorName, commit.authorEmailAddress)</td>
|
||||
<td width="80%">
|
||||
<span class="muted">@datetime(commit.authorTime):</span> @commit.shortMessage
|
||||
<span class="muted">@helper.html.datetimeago(commit.authorTime):</span> @commit.shortMessage
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</table>
|
||||
<input type="button" id="compare" value="Compare Revisions" class="btn"/>
|
||||
<input type="button" id="top" value="Back to Top" class="btn"/>
|
||||
<script>
|
||||
$(function(){
|
||||
$('input[name=commitId]').click(function(){
|
||||
|
@@ -12,17 +12,16 @@
|
||||
<li>
|
||||
<h1 class="wiki-title">@pageName</h1>
|
||||
<div class="small">
|
||||
<span class="muted"><strong>@page.committer</strong> edited this page at @datetime(page.time)</span>
|
||||
<span class="muted"><strong>@page.committer</strong> edited this page @helper.html.datetimeago(page.time)</span>
|
||||
</div>
|
||||
</li>
|
||||
<li class="pull-right">
|
||||
<div class="btn-group">
|
||||
@if(hasWritePermission){
|
||||
<a class="btn btn-small" href="@url(repository)/wiki/_new">New Page</a>
|
||||
@if(hasWritePermission){
|
||||
<div>
|
||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_edit">Edit Page</a>
|
||||
}
|
||||
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
|
||||
</div>
|
||||
<a class="btn btn-small btn-success" href="@url(repository)/wiki/_new">New Page</a>
|
||||
</div>
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
<div style="width: 200px;" class="pull-right">
|
||||
@@ -32,7 +31,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<ul style="margin-left: 0px; margin-bottom: 0px;">
|
||||
<ul style="margin-left: 0px; margin-bottom: 0px; word-break: break-all; width: 182px;">
|
||||
@pages.map { page =>
|
||||
<li style="margin-left:0px; list-style-type: none;"><a href="@url(repository)/wiki/@urlEncode(page)">@page</a></li>
|
||||
}
|
||||
|
@@ -119,6 +119,11 @@ div.container {
|
||||
width: 920px;
|
||||
}
|
||||
|
||||
div.container-wide {
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
div.pagination {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
@@ -608,11 +613,109 @@ li.highlight {
|
||||
background-color: #ffb;
|
||||
}
|
||||
|
||||
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 */
|
||||
/****************************************************************************/
|
||||
.nav-pills-group:after {
|
||||
display: table;
|
||||
line-height: 0;
|
||||
content: "";
|
||||
}
|
||||
|
||||
.nav-pills-group:after {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.nav-pills-group > li {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.nav-pills-group > li > a {
|
||||
padding-right: 12px;
|
||||
padding-left: 12px;
|
||||
line-height: 14px;
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nav-pills-group > li > a {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-left : 1px solid #e5e5e5;
|
||||
border-top : 1px solid #e5e5e5;
|
||||
border-bottom : 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.nav-pills-group > .first > a {
|
||||
-webkit-border-radius: 4px 0 0 4px;
|
||||
-moz-border-radius: 4px 0 0 4px;
|
||||
border-radius: 4px 0 0 4px;
|
||||
}
|
||||
|
||||
.nav-pills-group > .last > a {
|
||||
-webkit-border-radius: 0 4px 4px 0;
|
||||
-moz-border-radius: 0 4px 4px 0;
|
||||
border-radius: 0 4px 4px 0;
|
||||
border-right : 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.nav-pills-group > .active > a,
|
||||
.nav-pills-group > .active > a:hover,
|
||||
.nav-pills-group > .active > a:focus {
|
||||
color: #ffffff;
|
||||
background-color: #0088cc;
|
||||
border-color: #0088cc;
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
/* Issues */
|
||||
/****************************************************************************/
|
||||
.btn-group.open .dropdown-toggle.flat {
|
||||
background-image: none;
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
a.button-link {
|
||||
font-weight: normal;
|
||||
color: gray;
|
||||
}
|
||||
|
||||
a.selected {
|
||||
font-weight: bold;
|
||||
color: black;
|
||||
}
|
||||
|
||||
span.issue-status {
|
||||
display: block;
|
||||
font-size: large;
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
@@ -625,7 +728,7 @@ table.table-issues {
|
||||
a.issue-title {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
size: 110%;
|
||||
font-size: 120%;
|
||||
}
|
||||
|
||||
ul.label-list {
|
||||
@@ -662,8 +765,7 @@ span.milestone-alert {
|
||||
}
|
||||
|
||||
a.milestone-title {
|
||||
font-size: 120%;
|
||||
font-weight: bold;
|
||||
font-size: 180%;
|
||||
}
|
||||
|
||||
div.milestone-description {
|
||||
@@ -671,13 +773,12 @@ div.milestone-description {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
div.milestone-menu {
|
||||
font-size: 80%;
|
||||
a.milestone-title {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
div.milestone-menu a {
|
||||
margin-left: 8px;
|
||||
font-weight: bold;
|
||||
div.milestone-menu {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
div.milestone-menu a.delete {
|
||||
@@ -689,13 +790,13 @@ div#milestone-progress-area {
|
||||
}
|
||||
|
||||
div#milestone-progress-area div.milestone-progress {
|
||||
width: 150px;
|
||||
width: 130px;
|
||||
margin-bottom: -6px;
|
||||
}
|
||||
|
||||
div.milestone-progress {
|
||||
position: relative;
|
||||
height: 20px;
|
||||
height: 10px;
|
||||
color: white;
|
||||
margin-bottom: 4px;
|
||||
font-weight: bold;
|
||||
@@ -716,11 +817,6 @@ span.milestone-progress {
|
||||
-moz-border-radius: 4px;
|
||||
}
|
||||
|
||||
span.milestone-percentage {
|
||||
position: absolute;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
div.issue-header {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
@@ -805,6 +901,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 */
|
||||
/****************************************************************************/
|
||||
@@ -838,20 +948,24 @@ div.author-info div.committer {
|
||||
/****************************************************************************/
|
||||
/* Diff */
|
||||
/****************************************************************************/
|
||||
table.inlinediff {
|
||||
table.diff {
|
||||
font-size: 12px;
|
||||
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.inlinediff thead {
|
||||
table.diff thead {
|
||||
display: none;
|
||||
}
|
||||
|
||||
td.insert, td.equal, td.delete {
|
||||
table.inlinediff td.insert, table.inlinediff td.equal, table.inlinediff td.delete {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td.insert, td.equal, td.delete, td.empty {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
/****************************************************************************/
|
||||
/* Repository Settings */
|
||||
/****************************************************************************/
|
||||
|
Before Width: | Height: | Size: 166 B |
BIN
src/main/webapp/assets/common/images/clear.png
Normal file
After Width: | Height: | Size: 295 B |
BIN
src/main/webapp/assets/common/images/clear_hover.png
Normal file
After Width: | Height: | Size: 349 B |
BIN
src/main/webapp/assets/common/images/comment-active.png
Normal file
After Width: | Height: | Size: 95 B |
BIN
src/main/webapp/assets/common/images/comment.png
Normal file
After Width: | Height: | Size: 99 B |
BIN
src/main/webapp/assets/common/images/label_black.png
Normal file
After Width: | Height: | Size: 137 B |
BIN
src/main/webapp/assets/common/images/label_white.png
Normal file
After Width: | Height: | Size: 147 B |
BIN
src/main/webapp/assets/common/images/menu-feed.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/main/webapp/assets/common/images/milestone-active.png
Normal file
After Width: | Height: | Size: 104 B |