Merge branch 'master' into newui-for-pullreq

This commit is contained in:
Naoki Takezoe
2014-11-22 21:46:51 +09:00
63 changed files with 1871 additions and 2480 deletions

View File

@@ -80,6 +80,18 @@ Run the following commands in `Terminal` to
Release Notes Release Notes
-------- --------
### 2.6 - COMING SOON!
- Search box at issues and pull requests
- Information from administrator
- 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 ### 2.4.1 - 6 Oct 2014
- Bug fix - Bug fix

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 78 KiB

View File

@@ -1,18 +1,33 @@
package app package app
import service._ import service._
import util.{UsersAuthenticator, Keys} import util.{StringUtil, UsersAuthenticator, Keys}
import util.Implicits._ import util.Implicits._
import service.IssuesService.IssueSearchCondition
class DashboardController extends DashboardControllerBase class DashboardController extends DashboardControllerBase
with IssuesService with PullRequestService with RepositoryService with AccountService with IssuesService with PullRequestService with RepositoryService with AccountService
with UsersAuthenticator with UsersAuthenticator
trait DashboardControllerBase extends ControllerBase { 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 { get("/dashboard/issues")(usersOnly {
searchIssues("all") 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 { get("/dashboard/issues/assigned")(usersOnly {
@@ -23,87 +38,99 @@ trait DashboardControllerBase extends ControllerBase {
searchIssues("created_by") searchIssues("created_by")
}) })
get("/dashboard/issues/mentioned")(usersOnly {
searchIssues("mentioned")
})
get("/dashboard/pulls")(usersOnly { 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 { get("/dashboard/pulls/created_by")(usersOnly {
searchPullRequests("created_by", None) searchPullRequests("created_by")
}) })
get("/dashboard/pulls/public")(usersOnly { get("/dashboard/pulls/assigned")(usersOnly {
searchPullRequests("not_created_by", None) searchPullRequests("assigned")
}) })
get("/dashboard/pulls/for/:owner/:repository")(usersOnly { get("/dashboard/pulls/mentioned")(usersOnly {
searchPullRequests("all", Some(params("owner") + "/" + params("repository"))) 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) = { private def searchIssues(filter: String) = {
import IssuesService._ 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 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 userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
dashboard.html.issues( dashboard.html.issues(
dashboard.html.issueslist( searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*),
page, page,
countIssue(condition.copy(state = "open" ), filterUser, false, userRepos: _*), countIssue(condition.copy(state = "open" ), false, userRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, false, userRepos: _*), countIssue(condition.copy(state = "closed"), false, userRepos: _*),
condition), filter match {
countIssue(condition.copy(assigned = None, author = None), filterUser, false, userRepos: _*), case "assigned" => condition.copy(assigned = Some(userName))
countIssue(condition.copy(assigned = Some(userName), author = None), filterUser, false, userRepos: _*), case "mentioned" => condition.copy(mentioned = Some(userName))
countIssue(condition.copy(assigned = None, author = Some(userName)), filterUser, false, userRepos: _*), case _ => condition.copy(author = Some(userName))
countIssueGroupByRepository(condition, filterUser, false, userRepos: _*), },
condition, filter,
filter) getGroupNames(userName))
} }
private def searchPullRequests(filter: String, repository: Option[String]) = { private def searchPullRequests(filter: String) = {
import IssuesService._ import IssuesService._
import PullRequestService._ 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 userName = context.loginAccount.get.userName
val condition = getOrCreateCondition(Keys.Session.DashboardPulls, filter, userName)
val allRepos = getAllRepositories(userName) val allRepos = getAllRepositories(userName)
val userRepos = getUserRepositories(userName, context.baseUrl, true).map(repo => repo.owner -> repo.name)
val filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), filterUser, true, userRepos: _*)
dashboard.html.pulls( dashboard.html.pulls(
dashboard.html.pullslist( searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*),
page, page,
countIssue(condition.copy(state = "open" ), filterUser, true, allRepos: _*), countIssue(condition.copy(state = "open" ), true, allRepos: _*),
countIssue(condition.copy(state = "closed"), filterUser, true, allRepos: _*), countIssue(condition.copy(state = "closed"), true, allRepos: _*),
condition, filter match {
None, case "assigned" => condition.copy(assigned = Some(userName))
false), case "mentioned" => condition.copy(mentioned = Some(userName))
getAllPullRequestCountGroupByUser(condition.state == "closed", userName), case _ => condition.copy(author = Some(userName))
userRepos.map { case (userName, repoName) => },
(userName, repoName, counts.find { x => x._1 == userName && x._2 == repoName }.map(_._3).getOrElse(0)) filter,
}.sortBy(_._3).reverse, getGroupNames(userName))
condition,
filter)
} }

View File

@@ -9,7 +9,6 @@ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
import org.scalatra.Ok import org.scalatra.Ok
import model.Issue import model.Issue
import plugin.PluginSystem
class IssuesController extends IssuesControllerBase class IssuesController extends IssuesControllerBase
with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService with IssuesService with RepositoryService with AccountService with LabelsService with MilestonesService with ActivityService
@@ -50,7 +49,12 @@ trait IssuesControllerBase extends ControllerBase {
)(IssueStateForm.apply) )(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { repository => 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) searchIssues(repository)
}
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
@@ -195,7 +199,7 @@ trait IssuesControllerBase extends ControllerBase {
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("title" -> x.title, Map("title" -> x.title,
"content" -> view.Markdown.toHtml(x.content getOrElse "No description given.", "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 } else Unauthorized
@@ -212,7 +216,7 @@ trait IssuesControllerBase extends ControllerBase {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
Map("content" -> view.Markdown.toHtml(x.content, Map("content" -> view.Markdown.toHtml(x.content,
repository, false, true) repository, false, true, true, isEditable(x.userName, x.repositoryName, x.commentedUserName))
)) ))
} }
} else Unauthorized } else Unauthorized
@@ -390,19 +394,25 @@ trait IssuesControllerBase extends ControllerBase {
// retrieve search condition // retrieve search condition
val condition = session.putAndGet(sessionKey, val condition = session.putAndGet(sessionKey,
if(request.hasQueryString) IssueSearchCondition(request) if(request.hasQueryString){
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) 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( issues.html.list(
"issues", "issues",
searchIssue(condition, Map.empty, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), searchIssue(condition, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName),
page, page,
(getCollaborators(owner, repoName) :+ owner).sorted, (getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName), getMilestones(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), Map.empty, false, owner -> repoName), countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), Map.empty, false, owner -> repoName), countIssue(condition.copy(state = "closed"), false, owner -> repoName),
condition, condition,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))

View File

@@ -1,6 +1,6 @@
package app package app
import util.{LockUtil, CollaboratorsAuthenticator, JGitUtil, ReferrerAuthenticator, Notifier, Keys} import util._
import util.Directory._ import util.Directory._
import util.Implicits._ import util.Implicits._
import util.ControlUtil._ import util.ControlUtil._
@@ -18,6 +18,9 @@ import org.slf4j.LoggerFactory
import org.eclipse.jgit.merge.MergeStrategy import org.eclipse.jgit.merge.MergeStrategy
import org.eclipse.jgit.errors.NoMergeBaseException import org.eclipse.jgit.errors.NoMergeBaseException
import service.WebHookService.WebHookPayload import service.WebHookService.WebHookPayload
import util.JGitUtil.DiffInfo
import scala.Some
import util.JGitUtil.CommitInfo
class PullRequestsController extends PullRequestsControllerBase class PullRequestsController extends PullRequestsControllerBase
with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService
@@ -59,7 +62,12 @@ trait PullRequestsControllerBase extends ControllerBase {
case class MergeForm(message: String) case class MergeForm(message: String)
get("/:owner/:repository/pulls")(referrersOnly { repository => get("/:owner/:repository/pulls")(referrersOnly { 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) searchPullRequests(None, repository)
}
}) })
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
@@ -460,13 +468,13 @@ trait PullRequestsControllerBase extends ControllerBase {
issues.html.list( issues.html.list(
"pulls", "pulls",
searchIssue(condition, Map.empty, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
page, page,
(getCollaborators(owner, repoName) :+ owner).sorted, (getCollaborators(owner, repoName) :+ owner).sorted,
getMilestones(owner, repoName), getMilestones(owner, repoName),
getLabels(owner, repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), Map.empty, true, owner -> repoName), countIssue(condition.copy(state = "open" ), true, owner -> repoName),
countIssue(condition.copy(state = "closed"), Map.empty, true, owner -> repoName), countIssue(condition.copy(state = "closed"), true, owner -> repoName),
condition, condition,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))

View File

@@ -77,7 +77,9 @@ trait RepositoryViewerControllerBase extends ControllerBase {
contentType = "text/html" contentType = "text/html"
view.helpers.markdown(params("content"), repository, view.helpers.markdown(params("content"), repository,
params("enableWikiLink").toBoolean, 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, repo.html.commits(if(path.isEmpty) Nil else path.split("/").toList, branchName, repository,
logs.splitWith{ (commit1, commit2) => logs.splitWith{ (commit1, commit2) =>
view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime) view.helpers.date(commit1.commitTime) == view.helpers.date(commit2.commitTime)
}, page, hasNext) }, page, hasNext, hasWritePermission(repository.owner, repository.name, context.loginAccount))
case Left(_) => NotFound 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. * Deletes branch.
*/ */
@@ -331,7 +351,8 @@ trait RepositoryViewerControllerBase extends ControllerBase {
repo.html.files(revision, repository, repo.html.files(revision, repository,
if(path == ".") Nil else path.split("/").toList, // current path if(path == ".") Nil else path.split("/").toList, // current path
new JGitUtil.CommitInfo(lastModifiedCommit), // last modified commit 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 } getOrElse NotFound
} }

View File

@@ -21,6 +21,7 @@ trait SystemSettingsControllerBase extends ControllerBase {
private val form = mapping( private val form = mapping(
"baseUrl" -> trim(label("Base URL", optional(text()))), "baseUrl" -> trim(label("Base URL", optional(text()))),
"information" -> trim(label("Information", optional(text()))),
"allowAccountRegistration" -> trim(label("Account registration", boolean())), "allowAccountRegistration" -> trim(label("Account registration", boolean())),
"gravatar" -> trim(label("Gravatar", boolean())), "gravatar" -> trim(label("Gravatar", boolean())),
"notification" -> trim(label("Notification", boolean())), "notification" -> trim(label("Notification", boolean())),

View File

@@ -49,7 +49,7 @@ trait UserManagementControllerBase extends AccountManagementControllerBase {
"url" -> trim(label("URL" ,optional(text(maxlength(200))))), "url" -> trim(label("URL" ,optional(text(maxlength(200))))),
"fileId" -> trim(label("File ID" ,optional(text()))), "fileId" -> trim(label("File ID" ,optional(text()))),
"clearImage" -> trim(label("Clear image" ,boolean())), "clearImage" -> trim(label("Clear image" ,boolean())),
"removed" -> trim(label("Disable" ,boolean())) "removed" -> trim(label("Disable" ,boolean(disableByNotYourself("userName"))))
)(EditUserForm.apply) )(EditUserForm.apply)
val newGroupForm = mapping( 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
}
}
}
} }

View File

@@ -168,6 +168,11 @@ trait AccountService {
Repositories.filter(_.userName === userName.bind).delete 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 object AccountService extends AccountService

View File

@@ -47,9 +47,9 @@ trait IssuesService {
* @param repos Tuple of the repository owner and the repository name * @param repos Tuple of the repository owner and the repository name
* @return the count of the search result * @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 = 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. * Returns the Map which contains issue count for each labels.
@@ -62,7 +62,7 @@ trait IssuesService {
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition,
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = { 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) => .innerJoin(IssueLabels).on { (t1, t2) =>
t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) t1.byIssue(t2.userName, t2.repositoryName, t2.issueId)
} }
@@ -77,46 +77,22 @@ trait IssuesService {
} }
.toMap .toMap
} }
/**
* Returns list which contains issue count for each repository.
* If the issue does not exist, its repository is not included in the result.
*
* @param condition the search condition
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @param repos Tuple of the repository owner and the repository name
* @return list which contains issue count for each repository
*/
def countIssueGroupByRepository(
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean,
repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest)
.groupBy { t =>
t.userName -> t.repositoryName
}
.map { case (repo, t) =>
(repo._1, repo._2, t.length)
}
.sortBy(_._3 desc)
.list
}
/** /**
* Returns the search result against issues. * Returns the search result against issues.
* *
* @param condition the search condition * @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 pullRequest if true then returns only pull requests, false then returns only issues. * @param pullRequest if true then returns only pull requests, false then returns only issues.
* @param offset the offset for pagination * @param offset the offset for pagination
* @param limit the limit for pagination * @param limit the limit for pagination
* @param repos Tuple of the repository owner and the repository name * @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) * @return the search result (list of tuples which contain issue, labels and comment count)
*/ */
def searchIssue(condition: IssueSearchCondition, filterUser: Map[String, String], pullRequest: Boolean, def searchIssue(condition: IssueSearchCondition, pullRequest: Boolean, offset: Int, limit: Int, repos: (String, String)*)
offset: Int, limit: Int, repos: (String, String)*)
(implicit s: Session): List[IssueInfo] = { (implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels // get issues and comment count and labels
searchIssueQuery(repos, condition, filterUser, pullRequest) searchIssueQuery(repos, condition, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } .innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) }
.sortBy { case (t1, t2) => .sortBy { case (t1, t2) =>
(condition.sort match { (condition.sort match {
@@ -157,23 +133,18 @@ trait IssuesService {
/** /**
* Assembles query for conditional issue searching. * Assembles query for conditional issue searching.
*/ */
private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, private def searchIssueQuery(repos: Seq[(String, String)], condition: IssueSearchCondition, pullRequest: Boolean)(implicit s: Session) =
filterUser: Map[String, String], pullRequest: Boolean)(implicit s: Session) =
Issues filter { t1 => Issues filter { t1 =>
condition.repo repos
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
.getOrElse (repos)
.map { case (owner, repository) => t1.byRepository(owner, repository) } .map { case (owner, repository) => t1.byRepository(owner, repository) }
.foldLeft[Column[Boolean]](false) ( _ || _ ) && .foldLeft[Column[Boolean]](false) ( _ || _ ) &&
(t1.closed === (condition.state == "closed").bind) && (t1.closed === (condition.state == "closed").bind) &&
(t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) && (t1.milestoneId === condition.milestoneId.get.get.bind, condition.milestoneId.flatten.isDefined) &&
(t1.milestoneId.? isEmpty, condition.milestoneId == Some(None)) && (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.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) && (t1.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && (t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(t1.pullRequest === pullRequest.bind) && (t1.pullRequest === pullRequest.bind) &&
// Label filter
(IssueLabels filter { t2 => (IssueLabels filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) &&
(t2.labelId in (t2.labelId in
@@ -181,7 +152,19 @@ trait IssuesService {
(t3.byRepository(t1.userName, t1.repositoryName)) && (t3.byRepository(t1.userName, t1.repositoryName)) &&
(t3.labelName inSetBind condition.labels) (t3.labelName inSetBind condition.labels)
} map(_.labelId))) } 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], def createIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String],
@@ -340,31 +323,62 @@ object IssuesService {
milestoneId: Option[Option[Int]] = None, milestoneId: Option[Option[Int]] = None,
author: Option[String] = None, author: Option[String] = None,
assigned: Option[String] = None, assigned: Option[String] = None,
repo: Option[String] = None, mentioned: Option[String] = None,
state: String = "open", state: String = "open",
sort: String = "created", sort: String = "created",
direction: String = "desc"){ direction: String = "desc",
visibility: Option[String] = None,
groups: Set[String] = Set.empty){
def isEmpty: Boolean = { def isEmpty: Boolean = {
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty && labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty &&
state == "open" && sort == "created" && direction == "desc" state == "open" && sort == "created" && direction == "desc" && visibility.isEmpty
} }
def nonEmpty: Boolean = !isEmpty 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 = def toURL: String =
"?" + List( "?" + List(
if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))), if(labels.isEmpty) None else Some("labels=" + urlEncode(labels.mkString(","))),
milestoneId.map { id => "milestone=" + (id match { milestoneId.map { _ match {
case Some(x) => x.toString case Some(x) => "milestone=" + x
case None => "none" case None => "milestone=none"
})}, }},
author .map(x => "author=" + urlEncode(x)), author .map(x => "author=" + urlEncode(x)),
assigned.map(x => "assigned=" + urlEncode(x)), assigned .map(x => "assigned=" + urlEncode(x)),
repo.map("for=" + urlEncode(_)), mentioned.map(x => "mentioned=" + urlEncode(x)),
Some("state=" + urlEncode(state)), Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)), 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("&")
} }
@@ -375,19 +389,63 @@ object IssuesService {
if(value == null || value.isEmpty || (allow.nonEmpty && !allow.contains(value))) None else Some(value) 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 = def apply(request: HttpServletRequest): IssueSearchCondition =
IssueSearchCondition( IssueSearchCondition(
param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty), param(request, "labels").map(_.split(",").toSet).getOrElse(Set.empty),
param(request, "milestone").map{ param(request, "milestone").map {
case "none" => None case "none" => None
case x => x.toIntOpt case x => x.toIntOpt
}, },
param(request, "author"), param(request, "author"),
param(request, "assigned"), param(request, "assigned"),
param(request, "for"), param(request, "mentioned"),
param(request, "state", Seq("open", "closed")).getOrElse("open"), param(request, "state", Seq("open", "closed")).getOrElse("open"),
param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), 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 { def page(request: HttpServletRequest) = try {
val i = param(request, "page").getOrElse("1").toInt val i = param(request, "page").getOrElse("1").toInt

View File

@@ -36,23 +36,23 @@ trait PullRequestService { self: IssuesService =>
.list .list
.map { x => PullRequestCount(x._1, x._2) } .map { x => PullRequestCount(x._1, x._2) }
def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] = // def getAllPullRequestCountGroupByUser(closed: Boolean, userName: String)(implicit s: Session): List[PullRequestCount] =
PullRequests // PullRequests
.innerJoin(Issues).on { (t1, t2) => t1.byPrimaryKey(t2.userName, t2.repositoryName, t2.issueId) } // .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) } // .innerJoin(Repositories).on { case ((t1, t2), t3) => t2.byRepository(t3.userName, t3.repositoryName) }
.filter { case ((t1, t2), t3) => // .filter { case ((t1, t2), t3) =>
(t2.closed === closed.bind) && // (t2.closed === closed.bind) &&
( // (
(t3.isPrivate === false.bind) || // (t3.isPrivate === false.bind) ||
(t3.userName === userName.bind) || // (t3.userName === userName.bind) ||
(Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists) // (Collaborators.filter { t4 => t4.byRepository(t3.userName, t3.repositoryName) && (t4.collaboratorName === userName.bind)} exists)
) // )
} // }
.groupBy { case ((t1, t2), t3) => t2.openedUserName } // .groupBy { case ((t1, t2), t3) => t2.openedUserName }
.map { case (userName, t) => userName -> t.length } // .map { case (userName, t) => userName -> t.length }
.sortBy(_._2 desc) // .sortBy(_._2 desc)
.list // .list
.map { x => PullRequestCount(x._1, x._2) } // .map { x => PullRequestCount(x._1, x._2) }
def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int, def createPullRequest(originUserName: String, originRepositoryName: String, issueId: Int,
originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String, originBranch: String, requestUserName: String, requestRepositoryName: String, requestBranch: String,

View File

@@ -54,7 +54,6 @@ trait RepositoryService { self: AccountService =>
val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueComments = IssueComments.filter(_.byRepository(oldUserName, oldRepositoryName)).list
val issueLabels = IssueLabels .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 val collaborators = Collaborators.filter(_.byRepository(oldUserName, oldRepositoryName)).list
Repositories.filter { t => Repositories.filter { t =>
@@ -69,10 +68,17 @@ trait RepositoryService { self: AccountService =>
t.requestRepositoryName === oldRepositoryName.bind t.requestRepositoryName === oldRepositoryName.bind
}.map { t => t.requestUserName -> t.requestRepositoryName }.update(newUserName, newRepositoryName) }.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) deleteRepository(oldUserName, oldRepositoryName)
WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Milestones.insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*)
val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list
@@ -88,7 +94,7 @@ trait RepositoryService { self: AccountService =>
IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) IssueComments .insertAll(issueComments .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Labels .insertAll(labels .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*)
IssueLabels .insertAll(issueLabels .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){ if(account.isGroupAccount){
Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*) Collaborators.insertAll(getGroupMembers(newUserName).map(m => Collaborator(newUserName, newRepositoryName, m.userName)) :_*)
} else { } else {
@@ -96,12 +102,9 @@ trait RepositoryService { self: AccountService =>
} }
// Update activity messages // Update activity messages
val updateActivities = Activities.filter { t => Activities.filter { t =>
(t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || (t.message like s"%:${oldUserName}/${oldRepositoryName}]%") || (t.message like s"%:${oldUserName}/${oldRepositoryName}#%")
(t.message like s"%:${oldUserName}/${oldRepositoryName}#%") }.map { t => t.activityId -> t.message }.list.foreach { case (activityId, message) =>
}.map { t => t.activityId -> t.message }.list
updateActivities.foreach { case (activityId, message) =>
Activities.filter(_.activityId === activityId.bind).map(_.message).update( Activities.filter(_.activityId === activityId.bind).map(_.message).update(
message message
.replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]") .replace(s"[repo:${oldUserName}/${oldRepositoryName}]" ,s"[repo:${newUserName}/${newRepositoryName}]")

View File

@@ -12,6 +12,7 @@ trait SystemSettingsService {
def saveSystemSettings(settings: SystemSettings): Unit = { def saveSystemSettings(settings: SystemSettings): Unit = {
defining(new java.util.Properties()){ props => defining(new java.util.Properties()){ props =>
settings.baseUrl.foreach(x => props.setProperty(BaseURL, x.replaceFirst("/\\Z", ""))) 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(AllowAccountRegistration, settings.allowAccountRegistration.toString)
props.setProperty(Gravatar, settings.gravatar.toString) props.setProperty(Gravatar, settings.gravatar.toString)
props.setProperty(Notification, settings.notification.toString) props.setProperty(Notification, settings.notification.toString)
@@ -60,6 +61,7 @@ trait SystemSettingsService {
} }
SystemSettings( SystemSettings(
getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")), getOptionValue[String](props, BaseURL, None).map(x => x.replaceFirst("/\\Z", "")),
getOptionValue[String](props, Information, None),
getValue(props, AllowAccountRegistration, false), getValue(props, AllowAccountRegistration, false),
getValue(props, Gravatar, true), getValue(props, Gravatar, true),
getValue(props, Notification, false), getValue(props, Notification, false),
@@ -105,6 +107,7 @@ object SystemSettingsService {
case class SystemSettings( case class SystemSettings(
baseUrl: Option[String], baseUrl: Option[String],
information: Option[String],
allowAccountRegistration: Boolean, allowAccountRegistration: Boolean,
gravatar: Boolean, gravatar: Boolean,
notification: Boolean, notification: Boolean,
@@ -147,6 +150,7 @@ object SystemSettingsService {
val DefaultLdapPort = 389 val DefaultLdapPort = 389
private val BaseURL = "base_url" private val BaseURL = "base_url"
private val Information = "information"
private val AllowAccountRegistration = "allow_account_registration" private val AllowAccountRegistration = "allow_account_registration"
private val Gravatar = "gravatar" private val Gravatar = "gravatar"
private val Notification = "notification" private val Notification = "notification"

View File

@@ -53,6 +53,7 @@ object AutoUpdate {
* The history of versions. A head of this sequence is the current BitBucket version. * The history of versions. A head of this sequence is the current BitBucket version.
*/ */
val versions = Seq( val versions = Seq(
new Version(2, 5),
new Version(2, 4), new Version(2, 4),
new Version(2, 3) { new Version(2, 3) {
override def update(conn: Connection): Unit = { override def update(conn: Connection): Unit = {

View File

@@ -134,8 +134,8 @@ class CommitLogHook(owner: String, repository: String, pusher: String, baseUrl:
// Retrieve all issue count in the repository // Retrieve all issue count in the repository
val issueCount = val issueCount =
countIssue(IssueSearchCondition(state = "open"), Map.empty, false, owner -> repository) + countIssue(IssueSearchCondition(state = "open"), false, owner -> repository) +
countIssue(IssueSearchCondition(state = "closed"), Map.empty, false, owner -> repository) countIssue(IssueSearchCondition(state = "closed"), false, owner -> repository)
// Extract new commit and apply issue comment // Extract new commit and apply issue comment
val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch val defaultBranch = getRepository(owner, repository, baseUrl).get.repository.defaultBranch

View File

@@ -14,7 +14,7 @@ import org.eclipse.jgit.treewalk.filter._
import org.eclipse.jgit.diff.DiffEntry.ChangeType import org.eclipse.jgit.diff.DiffEntry.ChangeType
import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException} import org.eclipse.jgit.errors.{ConfigInvalidException, MissingObjectException}
import java.util.Date 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 service.RepositoryService
import org.eclipse.jgit.dircache.DirCacheEntry import org.eclipse.jgit.dircache.DirCacheEntry
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -507,6 +507,17 @@ object JGitUtil {
}.find(_._1 != null) }.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 = { def createDirCacheEntry(path: String, mode: FileMode, objectId: ObjectId): DirCacheEntry = {
val entry = new DirCacheEntry(path) val entry = new DirCacheEntry(path)
entry.setFileMode(mode) entry.setFileMode(mode)

View File

@@ -9,6 +9,7 @@ import org.pegdown.ast._
import org.pegdown.LinkRenderer.Rendering import org.pegdown.LinkRenderer.Rendering
import java.text.Normalizer import java.text.Normalizer
import java.util.Locale import java.util.Locale
import java.util.regex.Pattern
import scala.collection.JavaConverters._ import scala.collection.JavaConverters._
import service.{RequestCache, WikiService} import service.{RequestCache, WikiService}
@@ -18,17 +19,23 @@ object Markdown {
* Converts Markdown of Wiki pages to HTML. * Converts Markdown of Wiki pages to HTML.
*/ */
def toHtml(markdown: String, repository: service.RepositoryService.RepositoryInfo, 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 // escape issue id
val source = if(enableRefsLink){ val s = if(enableRefsLink){
markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2") markdown.replaceAll("(?<=(\\W|^))#(\\d+)(?=(\\W|$))", "issue:$2")
} else markdown } else markdown
// escape task list
val source = if(enableTaskList){
GitBucketHtmlSerializer.escapeTaskList(s)
} else s
val rootNode = new PegDownProcessor( val rootNode = new PegDownProcessor(
Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS Extensions.AUTOLINKS | Extensions.WIKILINKS | Extensions.FENCED_CODE_BLOCKS | Extensions.TABLES | Extensions.HARDWRAPS
).parseMarkdown(source.toCharArray) ).parseMarkdown(source.toCharArray)
new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink).toHtml(rootNode) new GitBucketHtmlSerializer(markdown, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission).toHtml(rootNode)
} }
} }
@@ -82,7 +89,9 @@ class GitBucketHtmlSerializer(
markdown: String, markdown: String,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableWikiLink: Boolean,
enableRefsLink: Boolean enableRefsLink: Boolean,
enableTaskList: Boolean,
hasWritePermission: Boolean
)(implicit val context: app.Context) extends ToHtmlSerializer( )(implicit val context: app.Context) extends ToHtmlSerializer(
new GitBucketLinkRender(context, repository, enableWikiLink), new GitBucketLinkRender(context, repository, enableWikiLink),
Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava Map[String, VerbatimSerializer](VerbatimSerializer.DEFAULT -> new GitBucketVerbatimSerializer).asJava
@@ -143,7 +152,10 @@ class GitBucketHtmlSerializer(
override def visit(node: TextNode): Unit = { override def visit(node: TextNode): Unit = {
// convert commit id and username to link. // 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) { if (abbreviations.isEmpty) {
printer.print(text) printer.print(text)
@@ -151,6 +163,28 @@ class GitBucketHtmlSerializer(
printWithAbbreviations(text) 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 { object GitBucketHtmlSerializer {
@@ -163,4 +197,14 @@ object GitBucketHtmlSerializer {
val noSpecialChars = StringUtil.urlEncode(normalized) val noSpecialChars = StringUtil.urlEncode(normalized)
noSpecialChars.toLowerCase(Locale.ENGLISH) 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 + "/>")
}
} }

View File

@@ -1,5 +1,5 @@
package view package view
import java.util.{Date, TimeZone} import java.util.{Locale, Date, TimeZone}
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import play.twirl.api.Html import play.twirl.api.Html
import util.StringUtil 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) 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'". * 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. * Converts Markdown of Wiki pages to HTML.
*/ */
def markdown(value: String, repository: service.RepositoryService.RepositoryInfo, def markdown(value: String, repository: service.RepositoryService.RepositoryInfo,
enableWikiLink: Boolean, enableRefsLink: Boolean)(implicit context: app.Context): Html = enableWikiLink: Boolean, enableRefsLink: Boolean, enableTaskList: Boolean = false, hasWritePermission: Boolean = false)(implicit context: app.Context): Html =
Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink)) Html(Markdown.toHtml(value, repository, enableWikiLink, enableRefsLink, enableTaskList, hasWritePermission))
def renderMarkup(filePath: List[String], fileContent: String, branch: String, def renderMarkup(filePath: List[String], fileContent: String, branch: String,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,

View File

@@ -25,7 +25,7 @@
@if(repository.repository.description.isDefined){ @if(repository.repository.description.isDefined){
<div>@repository.repository.description</div> <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>
</div> </div>
} }

View File

@@ -31,6 +31,14 @@
You can use this property to adjust URL difference between the reverse proxy and GitBucket. You can use this property to adjust URL difference between the reverse proxy and GitBucket.
</p> </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 --> <!-- Account registration -->
<!--====================================================================--> <!--====================================================================-->
<hr> <hr>

View File

@@ -16,6 +16,9 @@
<input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/> <input type="checkbox" name="removed" id="removed" value="true" @if(account.get.isRemoved){checked}/>
Disable Disable
</label> </label>
<div>
<span id="error-removed" class="error"></span>
</div>
} }
</fieldset> </fieldset>
@if(account.map(_.password.nonEmpty).getOrElse(true)){ @if(account.map(_.password.nonEmpty).getOrElse(true)){

View 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>&nbsp;&nbsp;
<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>

View File

@@ -1,50 +1,16 @@
@(listparts: play.twirl.api.Html, @(issues: List[service.IssuesService.IssueInfo],
allCount: Int, page: Int,
assignedCount: Int, openCount: Int,
createdByCount: Int, closedCount: Int,
repositories: List[(String, String, Int)],
condition: service.IssuesService.IssueSearchCondition, condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context) filter: String,
groups: List[String])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Your Issues"){ @html.main("Issues"){
<div class="container">
@dashboard.html.tab("issues") @dashboard.html.tab("issues")
<div class="row-fluid"> <div class="container">
<div class="span3"> @issuesnavi(filter, "issues", condition)
<ul class="nav nav-pills nav-stacked"> @issueslist(issues, page, openCount, closedCount, condition, filter, groups)
<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> </div>
@listparts
</div>
</div>
} }

View File

@@ -3,153 +3,33 @@
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
condition: service.IssuesService.IssueSearchCondition, condition: service.IssuesService.IssueSearchCondition,
collaborators: List[String] = Nil, filter: String,
milestones: List[model.Milestone] = Nil, groups: List[String])(implicit context: app.Context)
labels: List[model.Label] = Nil,
repository: Option[service.RepositoryService.RepositoryInfo] = None,
hasWritePermission: Boolean = false)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import service.IssuesService.IssueInfo @import service.IssuesService.IssueInfo
<div class="span9"> @*
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){ <ul class="nav nav-pills-group pull-left fill-width">
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter"> <li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/issues/created_by@condition.toURL">Created</a></li>
<i class="icon-remove-circle"></i> Clear milestone and label filters <li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/issues/assigned@condition.toURL">Assigned</a></li>
</a> <li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/issues/mentioned@condition.toURL">Mentioned</a></li>
} </ul>
@if(condition.repo.isDefined){ *@
<a href="@condition.copy(repo = None).toURL" id="clear-filter"> <table class="table table-bordered table-hover table-issues">
<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> <tr>
<td style="padding: 20px; background-color: #eee; text-align: center;"> <th style="background-color: #eee;">
No issues to show. @dashboard.html.header(openCount, closedCount, condition, groups)
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){ </th>
<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> </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;">&nbsp;</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.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>
}
}
</td>
</tr>
}
}
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) => @issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr> <tr>
<td> <td style="padding-top: 15px; padding-bottom: 15px;">
@if(hasWritePermission){
<label class="checkbox" style="cursor: default;">
<input type="checkbox" value="@issue.issueId"/>
}
@if(issue.isPullRequest){ @if(issue.isPullRequest){
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/> <img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
} else { } else {
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/> <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>&nbsp;&#xFF65; <a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
@if(issue.isPullRequest){ @if(issue.isPullRequest){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a> <a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else { } else {
@@ -162,23 +42,26 @@
@issue.assignedUserName.map { userName => @issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true) @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)&nbsp;
@if(commentCount > 0){ @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> <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> </div>
@if(hasWritePermission){
</label>
}
</td> </td>
</tr> </tr>
} }
</table> </table>
<div class="pull-right"> <div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL) @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div> </div>
</div>

View 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>

View File

@@ -1,42 +1,16 @@
@(listparts: play.twirl.api.Html, @(issues: List[service.IssuesService.IssueInfo],
counts: List[service.PullRequestService.PullRequestCount], page: Int,
repositories: List[(String, String, Int)], openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition, condition: service.IssuesService.IssueSearchCondition,
filter: String)(implicit context: app.Context) filter: String,
groups: List[String])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main("Your Issues"){ @html.main("Pull Requests"){
<div class="container">
@dashboard.html.tab("pulls") @dashboard.html.tab("pulls")
<div class="row-fluid"> <div class="container">
<div class="span3"> @issuesnavi(filter, "pulls", condition)
<ul class="nav nav-pills nav-stacked"> @pullslist(issues, page, openCount, closedCount, condition, filter, groups)
<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> </div>
@listparts
</div>
</div>
} }

View File

@@ -3,75 +3,42 @@
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
condition: service.IssuesService.IssueSearchCondition, condition: service.IssuesService.IssueSearchCondition,
repository: Option[service.RepositoryService.RepositoryInfo], filter: String,
hasWritePermission: Boolean)(implicit context: app.Context) groups: List[String])(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import service.IssuesService.IssueInfo @import service.IssuesService.IssueInfo
<div class="span9"> @*
@repository.map { repository => <ul class="nav nav-pills-group pull-left fill-width">
@if(hasWritePermission){ <li class="@if(filter == "created_by"){active} first"><a href="@path/dashboard/pulls/created_by@condition.toURL">Created</a></li>
<div class="pull-right"> <li class="@if(filter == "assigned"){active}"><a href="@path/dashboard/pulls/assigned@condition.toURL">Assigned</a></li>
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 7, condition.toURL) <li class="@if(filter == "mentioned"){active} last"><a href="@path/dashboard/pulls/mentioned@condition.toURL">Mentioned</a></li>
<a href="@url(repository)/compare" class="btn btn-small btn-success">New pull request</a> <li class="pull-right">
</div> <div class="input-prepend" style="margin-bottom: 0px;">
}
}
<div class="btn-group"> <div class="btn-group">
<a class="btn btn-small@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a> <button type="button" class="btn dropdown-toggle" data-toggle="dropdown" style="height: 34px;">
<a class="btn btn-small@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a> 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> </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>
<li> </ul>
<a href="@condition.copy(sort="created", direction="asc" ).toURL"> *@
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest <table class="table table-bordered table-hover table-issues">
</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> <tr>
<td style="padding: 20px; background-color: #eee; text-align: center;"> <th style="background-color: #eee;">
No pull requests to show. @dashboard.html.header(openCount, closedCount, condition, groups)
</td> </th>
</tr> </tr>
}
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) => @issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr> <tr>
<td> <td>
@@ -94,8 +61,7 @@
</td> </td>
</tr> </tr>
} }
</table> </table>
<div class="pull-right"> <div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL) @helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.PullRequestService.PullRequestLimit, 10, condition.toURL)
</div>
</div> </div>

View File

@@ -1,13 +1,47 @@
@(active: String = "")(implicit context: app.Context) @(active: String = "")(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<ul class="nav nav-tabs"> <div class="dashboard-nav">
<li@if(active == ""){ class="active"}><a href="@path/">News Feed</a></li> <div class="container">
<a href="@path/" @if(active == ""){ class="active"}>
<img src="@assets/common/images/menu-feed.png">
News Feed
</a>
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<li@if(active == "pulls" ){ class="active"}><a href="@path/dashboard/pulls">Pull Requests</a></li> <a href="@path/dashboard/pulls" @if(active == "pulls" ){ class="active"}>
<li@if(active == "issues"){ class="active"}><a href="@path/dashboard/issues/repos">Issues</a></li> <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>
} }
@if(active == ""){ </div>
<li class="pull-right"><a href="@path/activities.atom"><img src="@assets/common/images/feed.png" alt="activities"></a></li> </div>
} <style type="text/css">
</ul> 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>

View File

@@ -62,7 +62,7 @@
@detailActivity(activity: model.Activity, image: String) = { @detailActivity(activity: model.Activity, image: String) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div> <div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content"> <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"> <div class="strong">
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
@@ -76,7 +76,7 @@
@customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = { @customActivity(activity: model.Activity, image: String)(additionalInfo: Any) = {
<div class="activity-icon-large"><img src="@assets/common/images/@image"/></div> <div class="activity-icon-large"><img src="@assets/common/images/@image"/></div>
<div class="activity-content"> <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"> <div class="strong">
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
@@ -91,7 +91,7 @@
<div> <div>
@avatar(activity.activityUserName, 16) @avatar(activity.activityUserName, 16)
@activityMessage(activity.message) @activityMessage(activity.message)
<span class="muted small">@datetime(activity.activityDate)</span> <span class="muted small">@helper.html.datetimeago(activity.activityDate)</span>
</div> </div>
</div> </div>
} }

View 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">&times</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:&nbsp;<span class="new-branch"></span></span>
<br><span style="padding-left: 17px;">from&nbsp;'@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>

View 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>

View File

@@ -0,0 +1,7 @@
@(error: Option[Any])
@if(error.isDefined){
<div class='alert alert-danger'>
<button type="button" class="close" data-dismiss="alert">&times;</button>
@error
</div>
}

View File

@@ -1,7 +1,7 @@
@(info: Option[Any]) @(info: Option[Any])
@if(info.isDefined){ @if(info.isDefined){
<div class="alert alert-info"> <div class="alert alert-info">
<button type="button" class="close" data-dismiss="alert">×</button> <button type="button" class="close" data-dismiss="alert">&times;</button>
@info @info
</div> </div>
} }

View File

@@ -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) style: String = "", placeholder: String = "Leave a comment", elastic: Boolean = false)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@@ -38,7 +38,8 @@ $(function(){
$.post('@url(repository)/_preview', { $.post('@url(repository)/_preview', {
content : $('#content').val(), content : $('#content').val(),
enableWikiLink : @enableWikiLink, enableWikiLink : @enableWikiLink,
enableRefsLink : @enableRefsLink enableRefsLink : @enableRefsLink,
enableTaskList : @enableTaskList
}, function(data){ }, function(data){
$('#preview-area').html(data); $('#preview-area').html(data);
prettyPrint(); prettyPrint();

View File

@@ -4,13 +4,23 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@main("GitBucket"){ @main("GitBucket"){
<div class="container">
@dashboard.html.tab() @dashboard.html.tab()
<div class="container">
<div class="row-fluid"> <div class="row-fluid">
<div class="span8"> <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) @helper.html.activities(activities)
</div> </div>
<div class="span4"> <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">&times;</button>
@Html(information)
</div>
}
@if(loginAccount.isEmpty){ @if(loginAccount.isEmpty){
@signinform(settings) @signinform(settings)
} else { } else {

View File

@@ -10,7 +10,7 @@
<div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div> <div class="issue-avatar-image">@avatar(loginAccount.get.userName, 48)</div>
<div class="box issue-comment-box"> <div class="box issue-comment-box">
<div class="box-content"> <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> </div>
<div class="pull-right"> <div class="pull-right">

View File

@@ -8,7 +8,7 @@
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div> <div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
<div class="box issue-comment-box"> <div class="box issue-comment-box">
<div class="box-header-small"> <div class="box-header-small">
@user(issue.openedUserName, styleClass="username strong") <span class="muted">commented on @datetime(issue.registeredDate)</span> @user(issue.openedUserName, styleClass="username strong") <span class="muted">commented @helper.html.datetimeago(issue.registeredDate)</span>
<span class="pull-right"> <span class="pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){ @if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a href="#" data-issue-id="@issue.issueId"><i class="icon-pencil"></i></a> <a href="#" data-issue-id="@issue.issueId"><i class="icon-pencil"></i></a>
@@ -16,7 +16,7 @@
</span> </span>
</div> </div>
<div class="box-content issue-content" id="issueContent"> <div class="box-content issue-content" id="issueContent">
@markdown(issue.content getOrElse "No description provided.", repository, false, true) @markdown(issue.content getOrElse "No description provided.", repository, false, true, true, hasWritePermission)
</div> </div>
</div> </div>
@@ -32,7 +32,7 @@
} else { } else {
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request } @if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
} }
on @datetime(comment.registeredDate) @helper.html.datetimeago(comment.registeredDate)
</span> </span>
<span class="pull-right"> <span class="pull-right">
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" && @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" &&
@@ -46,7 +46,7 @@
@if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){ @if(comment.action == "commit" && comment.content.split(" ").last.matches("[a-f0-9]{40}")){
@defining(comment.content.substring(comment.content.length - 40)){ id => @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> <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 { } else {
@if(comment.action == "refer"){ @if(comment.action == "refer"){
@@ -54,7 +54,7 @@
<strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong> <strong><a href="@path/@repository.owner/@repository.name/issues/@issueId">Issue #@issueId</a>: @rest.mkString(":")</strong>
} }
} else { } else {
@markdown(comment.content, repository, false, true) @markdown(comment.content, repository, false, true, true, hasWritePermission)
} }
} }
</div> </div>
@@ -70,7 +70,7 @@
} else { } 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> to <span class="label label-info monospace">@pullreq.map(_.requestUserName):@pullreq.map(_.requestBranch)</span>
} }
@datetime(comment.registeredDate) @helper.html.datetimeago(comment.registeredDate)
</div> </div>
} }
@if(comment.action == "close" || comment.action == "close_comment"){ @if(comment.action == "close" || comment.action == "close_comment"){
@@ -78,9 +78,9 @@
<span class="label label-important">Closed</span> <span class="label label-important">Closed</span>
@avatar(comment.commentedUserName, 20) @avatar(comment.commentedUserName, 20)
@if(issue.isPullRequest){ @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 { } 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> </div>
} }
@@ -88,14 +88,14 @@
<div class="small issue-comment-action"> <div class="small issue-comment-action">
<span class="label label-success">Reopened</span> <span class="label label-success">Reopened</span>
@avatar(comment.commentedUserName, 20) @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> </div>
} }
@if(comment.action == "delete_branch"){ @if(comment.action == "delete_branch"){
<div class="small issue-comment-action"> <div class="small issue-comment-action">
<span class="label">Deleted</span> <span class="label">Deleted</span>
@avatar(comment.commentedUserName, 20) @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> </div>
} }
} }
@@ -134,5 +134,67 @@ $(function(){
} }
return false; 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>

View File

@@ -7,7 +7,7 @@
@import view.helpers._ @import view.helpers._
@html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"New Issue - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu("issues", repository){
@tab("issues", false, repository) @navigation("issues", false, repository)
<br/><br/><hr style="margin-bottom: 10px;"> <br/><br/><hr style="margin-bottom: 10px;">
<form action="@url(repository)/issues/new" method="POST" validate="true"> <form action="@url(repository)/issues/new" method="POST" validate="true">
<div class="row-fluid"> <div class="row-fluid">
@@ -57,7 +57,7 @@
</div> </div>
</div> </div>
<hr> <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> </div>
<div class="pull-right"> <div class="pull-right">

View File

@@ -28,7 +28,7 @@
<span class="label label-success issue-status">Open</span> <span class="label label-success issue-status">Open</span>
} }
<span class="muted"> <span class="muted">
@user(issue.openedUserName, styleClass="username strong") opened this issue on @datetime(issue.registeredDate) - @defining( @user(issue.openedUserName, styleClass="username strong") opened this issue @helper.html.datetimeago(issue.registeredDate) - @defining(
comments.filter( _.action.contains("comment") ).size comments.filter( _.action.contains("comment") ).size
){ count => ){ count =>
@count @plural(count, "comment") @count @plural(count, "comment")

View File

@@ -6,8 +6,8 @@
@import view.helpers._ @import view.helpers._
@html.main(s"Labels - ${repository.owner}/${repository.name}"){ @html.main(s"Labels - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){ @html.menu("issues", repository){
@issues.html.tab("labels", hasWritePermission, repository) @issues.html.navigation("labels", hasWritePermission, repository)
&nbsp; <br>
<table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;"> <table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;">
<tr><td></td></tr> <tr><td></td></tr>
</table> </table>

View File

@@ -13,7 +13,7 @@
@import view.helpers._ @import view.helpers._
@html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){ @html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu(target, repository){ @html.menu(target, repository){
@tab(target, true, repository) @navigation(target, true, repository, Some(condition))
@listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission) @listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
@if(hasWritePermission){ @if(hasWritePermission){
<form id="batcheditForm" method="POST"> <form id="batcheditForm" method="POST">

View File

@@ -12,6 +12,7 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import service.IssuesService.IssueInfo @import service.IssuesService.IssueInfo
<br>
@if(condition.nonEmpty){ @if(condition.nonEmpty){
<div> <div>
<a href="@service.IssuesService.IssueSearchCondition().toURL" class="header-link"> <a href="@service.IssuesService.IssueSearchCondition().toURL" class="header-link">
@@ -21,7 +22,6 @@
</a> </a>
</div> </div>
} }
&nbsp;
<table class="table table-bordered table-hover table-issues"> <table class="table table-bordered table-hover table-issues">
<tr> <tr>
<th style="background-color: #eee;"> <th style="background-color: #eee;">
@@ -203,7 +203,7 @@
} }
</span> </span>
<div class="small muted" style="margin-left: 40px; margin-top: 5px;"> <div class="small muted" style="margin-left: 40px; margin-top: 5px;">
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate) #@issue.issueId opened @helper.html.datetimeago(issue.registeredDate) by @user(issue.openedUserName, styleClass="username")
@milestone.map { milestone => @milestone.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> <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>
} }

View File

@@ -7,7 +7,7 @@
<h4>New milestone</h4> <h4>New milestone</h4>
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div> <div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
} else { } else {
@issues.html.tab("milestones", false, repository) @issues.html.navigation("milestones", false, repository)
<br><br> <br><br>
} }
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/> <hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>

View File

@@ -6,8 +6,8 @@
@import view.helpers._ @import view.helpers._
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){ @html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){ @html.menu("issues", repository){
@issues.html.tab("milestones", hasWritePermission, repository) @issues.html.navigation("milestones", hasWritePermission, repository)
&nbsp; <br>
<table class="table table-bordered table-hover table-issues"> <table class="table table-bordered table-hover table-issues">
<tr> <tr>
<th style="background-color: #eee;"> <th style="background-color: #eee;">
@@ -34,7 +34,7 @@
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a> <a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a>
<div style="margin-top: 6px"> <div style="margin-top: 6px">
@if(milestone.closedDate.isDefined){ @if(milestone.closedDate.isDefined){
<span class="muted">Closed @datetime(milestone.closedDate.get)</span> <span class="muted">Closed @helper.html.datetimeago(milestone.closedDate.get)</span>
} else { } else {
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){

View 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>

View File

@@ -1,30 +0,0 @@
@(active: String, newButton: Boolean,
repository: service.RepositoryService.RepositoryInfo)(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>
@if(loginAccount.isDefined){
<li class="pull-right">
<div class="btn-group">
@if(newButton){
@if(active == "issues"){
<a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
}
@if(active == "pulls"){
<a class="btn btn-success" href="@url(repository)/compare">New pull request</a>
}
@if(active == "labels"){
<a class="btn btn-success" href="javascript:void(0);" id="new-label-button">New label</a>
}
@if(active == "milestones"){
<a class="btn btn-success" href="@url(repository)/issues/milestones/new">New milestone</a>
}
}
</div>
</li>
}
</ul>

View File

@@ -1,7 +1,9 @@
@(active: String, @(active: String,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
id: Option[String] = None, 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 context._
@import view.helpers._ @import view.helpers._
@@ -31,6 +33,8 @@
} }
<div class="container"> <div class="container">
@helper.html.information(info)
@helper.html.error(error)
@if(repository.commitCount > 0){ @if(repository.commitCount > 0){
<div class="pull-right"> <div class="pull-right">
<div class="input-prepend"> <div class="input-prepend">

View File

@@ -58,7 +58,7 @@
<div style="width: 600px; border-right: 1px solid #d4d4d4;"> <div style="width: 600px; border-right: 1px solid #d4d4d4;">
<span class="error" id="error-title"></span> <span class="error" id="error-title"></span>
<input type="text" name="title" style="width: 580px" placeholder="Title"/> <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="targetUserName" value="@originRepository.owner"/>
<input type="hidden" name="targetBranch" value="@originId"/> <input type="hidden" name="targetBranch" value="@originId"/>
<input type="hidden" name="requestUserName" value="@forkedRepository.owner"/> <input type="hidden" name="requestUserName" value="@forkedRepository.owner"/>

View File

@@ -20,7 +20,7 @@
<span class="label label-info">Merged</span> <span class="label label-info">Merged</span>
@user(comment.commentedUserName, styleClass="username strong") merged @commits.size @plural(commits.size, "commit") @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> into <code>@pullreq.userName:@pullreq.branch</code> from <code>@pullreq.requestUserName:@pullreq.requestBranch</code>
at @datetime(comment.registeredDate) @helper.html.datetimeago(comment.registeredDate)
}.getOrElse { }.getOrElse {
<span class="label label-important">Closed</span> <span class="label label-important">Closed</span>
@user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit") @user(issue.openedUserName, styleClass="username strong") wants to merge @commits.size @plural(commits.size, "commit")

View File

@@ -9,10 +9,10 @@
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository){ @html.menu("code", repository){
<div class="head"> <div class="head">
@helper.html.dropdown( @helper.html.branchcontrol(
value = if(branch.length == 40) branch.substring(0, 10) else branch, branch,
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", repository,
mini = true hasWritePermission
){ ){
@repository.branchList.map { x => @repository.branchList.map { x =>
<li><a href="@url(repository)/blob/@encodeRefName(x)/@pathList.mkString("/")">@helper.html.checkicon(x == branch) @x</a></li> <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"> <div class="pull-left">
@avatar(latestCommit, 20) @avatar(latestCommit, 20)
@user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong") @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> <a href="@url(repository)/commit/@latestCommit.id" class="commit-message">@link(latestCommit.summary, repository)</a>
</div> </div>
<div class="btn-group pull-right"> <div class="btn-group pull-right">

View File

@@ -22,7 +22,7 @@
} }
</td> </td>
<td> <td>
@datetime(latestUpdateDate) @helper.html.datetimeago(latestUpdateDate, false)
</td> </td>
<td> <td>
@if(repository.repository.defaultBranch == branchName){ @if(repository.repository.defaultBranch == branchName){

View File

@@ -68,13 +68,13 @@
<div class="author"> <div class="author">
@avatar(commit, 20) @avatar(commit, 20)
<span>@user(commit.authorName, commit.authorEmailAddress, "username strong")</span> <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> </div>
@if(commit.isDifferentFromAuthor) { @if(commit.isDifferentFromAuthor) {
<div class="committer"> <div class="committer">
<span class="icon-arrow-right"></span> <span class="icon-arrow-right"></span>
<span>@user(commit.committerName, commit.committerEmailAddress, "username strong")</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>
} }
</div> </div>

View File

@@ -3,16 +3,17 @@
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
commits: Seq[Seq[util.JGitUtil.CommitInfo]], commits: Seq[Seq[util.JGitUtil.CommitInfo]],
page: Int, page: Int,
hasNext: Boolean)(implicit context: app.Context) hasNext: Boolean,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @html.main(s"${repository.owner}/${repository.name}", Some(repository)) {
@html.menu("code", repository){ @html.menu("code", repository){
<div class="head"> <div class="head">
@helper.html.dropdown( @helper.html.branchcontrol(
value = if(branch.length == 40) branch.substring(0, 10) else branch, branch,
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", repository,
mini = true hasWritePermission
){ ){
@repository.branchList.map { x => @repository.branchList.map { x =>
<li><a href="@url(repository)/commits/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li> <li><a href="@url(repository)/commits/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li>
@@ -58,11 +59,11 @@
} }
<div class="small"> <div class="small">
@user(commit.authorName, commit.authorEmailAddress, "username") @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) { @if(commit.isDifferentFromAuthor) {
<span class="icon-arrow-right" style="margin-top : -2px ;"></span> <span class="icon-arrow-right" style="margin-top : -2px ;"></span>
@user(commit.committerName, commit.committerEmailAddress, "username") @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>
</div> </div>

View File

@@ -4,16 +4,18 @@
latestCommit: util.JGitUtil.CommitInfo, latestCommit: util.JGitUtil.CommitInfo,
files: List[util.JGitUtil.FileInfo], files: List[util.JGitUtil.FileInfo],
readme: Option[(List[String], String)], 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 context._
@import view.helpers._ @import view.helpers._
@html.main(s"${repository.owner}/${repository.name}", Some(repository)) { @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"> <div class="head">
@helper.html.dropdown( @helper.html.branchcontrol(
value = if(branch.length == 40) branch.substring(0, 10) else branch, branch,
prefix = if(branch.length == 40) "tree" else if(repository.branchList.contains(branch)) "branch" else "tree", repository,
mini = true hasWritePermission
){ ){
@repository.branchList.map { x => @repository.branchList.map { x =>
<li><a href="@url(repository)/tree/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li> <li><a href="@url(repository)/tree/@encodeRefName(x)">@helper.html.checkicon(x == branch) @x</a></li>
@@ -47,13 +49,13 @@
<div class="author"> <div class="author">
@avatar(latestCommit, 20) @avatar(latestCommit, 20)
<span>@user(latestCommit.authorName, latestCommit.authorEmailAddress, "username strong")</span> <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> </div>
@if(latestCommit.isDifferentFromAuthor) { @if(latestCommit.isDifferentFromAuthor) {
<div class="committer"> <div class="committer">
<span class="icon-arrow-right"></span> <span class="icon-arrow-right"></span>
<span>@user(latestCommit.committerName, latestCommit.committerEmailAddress, "username strong")</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>
} }
</div> </div>
@@ -106,7 +108,7 @@
<a href="@url(repository)/commit/@file.commitId" class="commit-message">@link(file.message, repository)</a> <a href="@url(repository)/commit/@file.commitId" class="commit-message">@link(file.message, repository)</a>
[@user(file.author, file.mailAddress)] [@user(file.author, file.mailAddress)]
</td> </td>
<td style="text-align: right;">@datetime(file.time)</td> <td style="text-align: right;">@helper.html.datetimeago(file.time, false)</td>
</tr> </tr>
} }
</table> </table>

View File

@@ -14,7 +14,7 @@
@repository.tags.reverse.map { tag => @repository.tags.reverse.map { tag =>
<tr> <tr>
<td><a href="@url(repository)/tree/@encodeRefName(tag.name)">@tag.name</a></td> <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 class="monospace"><a href="@url(repository)/commit/@tag.id">@tag.id.substring(0, 10)</a></td>
<td> <td>
<a href="@url(repository)/archive/@{encodeRefName(tag.name)}.zip">ZIP</a> <a href="@url(repository)/archive/@{encodeRefName(tag.name)}.zip">ZIP</a>

View File

@@ -16,7 +16,7 @@
@files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file => @files.drop((page - 1) * CodeLimit).take(CodeLimit).map { file =>
<div> <div>
<h5><a href="@url(repository)/blob/@repository.repository.defaultBranch/@file.path">@file.path</a></h5> <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> <pre class="prettyprint linenums:@file.highlightLineNumber" style="padding-left: 25px;">@Html(file.highlightText)</pre>
</div> </div>
} }

View File

@@ -22,7 +22,7 @@
} }
<div class="small muted"> <div class="small muted">
Opened by <a href="@url(issue.openedUserName)" class="username">@issue.openedUserName</a> 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){ @if(issue.commentCount > 0){
&nbsp;&nbsp;<i class="icon-comment"></i><span class="strong">@issue.commentCount</span> @plural(issue.commentCount, "comment") &nbsp;&nbsp;<i class="icon-comment"></i><span class="strong">@issue.commentCount</span> @plural(issue.commentCount, "comment")
} }

View File

@@ -10,19 +10,19 @@
<h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1> <h1 class="wiki-title"><span class="muted">Editing</span> @if(pageName.isEmpty){New Page} else {@pageName}</h1>
</li> </li>
<li class="pull-right"> <li class="pull-right">
<div class="btn-group"> <div>
@if(page.isDefined){ @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)/_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" 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> </div>
</li> </li>
</ul> </ul>
<form action="@url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true"> <form action="@url(repository)/wiki/@if(page.isEmpty){_new} else {_edit}" method="POST" validate="true">
<span id="error-pageName" class="error"></span> <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."/> <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="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="currentPageName" value="@pageName"/>
<input type="hidden" name="id" value="@page.map(_.id)"/> <input type="hidden" name="id" value="@page.map(_.id)"/>

View File

@@ -16,33 +16,39 @@
</h1> </h1>
</li> </li>
<li class="pull-right"> <li class="pull-right">
<div class="btn-group"> <div>
@if(pageName.isEmpty){ @if(pageName.isEmpty){
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<a class="btn btn-small" href="@url(repository)/wiki/_new">New Page</a> <a class="btn btn-small" href="@url(repository)/wiki/_new">New Page</a>
} }
} else { } else {
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)">View Page</a>
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<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)/_edit">Edit Page</a>
<a class="btn btn-small btn-success" href="@url(repository)/wiki/_new">New Page</a>
} }
} }
</div> </div>
</li> </li>
</ul> </ul>
<table class="table table-bordered fill-width pull-left"> <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 => @commits.map { commit =>
<tr> <tr>
<td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td> <td width="0%"><input type="checkbox" name="commitId" value="@commit.id"></td>
<td>@avatar(commit, 20)&nbsp;@user(commit.authorName, commit.authorEmailAddress)</td> <td>@avatar(commit, 20)&nbsp;@user(commit.authorName, commit.authorEmailAddress)</td>
<td width="80%"> <td width="80%">
<span class="muted">@datetime(commit.authorTime):</span>&nbsp;@commit.shortMessage <span class="muted">@helper.html.datetimeago(commit.authorTime):</span>&nbsp;@commit.shortMessage
</td> </td>
</tr> </tr>
} }
</table> </table>
<input type="button" id="compare" value="Compare Revisions" class="btn"/>
<input type="button" id="top" value="Back to Top" class="btn"/>
<script> <script>
$(function(){ $(function(){
$('input[name=commitId]').click(function(){ $('input[name=commitId]').click(function(){

View File

@@ -12,17 +12,16 @@
<li> <li>
<h1 class="wiki-title">@pageName</h1> <h1 class="wiki-title">@pageName</h1>
<div class="small"> <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> </div>
</li> </li>
<li class="pull-right"> <li class="pull-right">
<div class="btn-group">
@if(hasWritePermission){ @if(hasWritePermission){
<a class="btn btn-small" href="@url(repository)/wiki/_new">New Page</a> <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)/_edit">Edit Page</a>
} <a class="btn btn-small btn-success" href="@url(repository)/wiki/_new">New Page</a>
<a class="btn btn-small" href="@url(repository)/wiki/@urlEncode(pageName)/_history">Page History</a>
</div> </div>
}
</li> </li>
</ul> </ul>
<div style="width: 200px;" class="pull-right"> <div style="width: 200px;" class="pull-right">

View File

@@ -617,6 +617,30 @@ span.simplified-path {
color: #888; 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 pulls group */
/****************************************************************************/ /****************************************************************************/
@@ -877,6 +901,20 @@ div.attachable div.clickable {
background-color: white; 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 */ /* Pull Request */
/****************************************************************************/ /****************************************************************************/

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@@ -25,4 +25,69 @@ class GitBucketHtmlSerializerSpec extends Specification {
after mustEqual "foo%21bar%40baz%3e9000" after mustEqual "foo%21bar%40baz%3e9000"
} }
} }
"escapeTaskList" should {
"convert '- [ ] ' to '* task: :'" in {
val before = "- [ ] aaaa"
val after = escapeTaskList(before)
after mustEqual "* task: : aaaa"
}
"convert ' - [ ] ' to ' * task: :'" in {
val before = " - [ ] aaaa"
val after = escapeTaskList(before)
after mustEqual " * task: : aaaa"
}
"convert only first '- [ ] '" in {
val before = " - [ ] aaaa - [ ] bbb"
val after = escapeTaskList(before)
after mustEqual " * task: : aaaa - [ ] bbb"
}
"convert '- [x] ' to '* task:x:'" in {
val before = " - [x] aaaa"
val after = escapeTaskList(before)
after mustEqual " * task:x: aaaa"
}
"convert multi lines" in {
val before = """
tasks
- [x] aaaa
- [ ] bbb
"""
val after = escapeTaskList(before)
after mustEqual """
tasks
* task:x: aaaa
* task: : bbb
"""
}
"no convert if inserted before '- [ ] '" in {
val before = " a - [ ] aaaa"
val after = escapeTaskList(before)
after mustEqual " a - [ ] aaaa"
}
"no convert '- [] '" in {
val before = " - [] aaaa"
val after = escapeTaskList(before)
after mustEqual " - [] aaaa"
}
"no convert '- [ ]a'" in {
val before = " - [ ]a aaaa"
val after = escapeTaskList(before)
after mustEqual " - [ ]a aaaa"
}
"no convert '-[ ] '" in {
val before = " -[ ] aaaa"
val after = escapeTaskList(before)
after mustEqual " -[ ] aaaa"
}
}
} }