Merge branch 'new-issue-ui'

This commit is contained in:
Naoki Takezoe
2014-10-06 01:54:19 +09:00
48 changed files with 1358 additions and 936 deletions

View File

@@ -34,9 +34,9 @@
borderopacity="1.0" borderopacity="1.0"
inkscape:pageopacity="0.0" inkscape:pageopacity="0.0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:zoom="0.7" inkscape:zoom="0.98994949"
inkscape:cx="482.58197" inkscape:cx="174.78739"
inkscape:cy="-83.92636" inkscape:cy="-195.96338"
inkscape:document-units="px" inkscape:document-units="px"
inkscape:current-layer="layer1-9" inkscape:current-layer="layer1-9"
showgrid="false" showgrid="false"
@@ -1583,7 +1583,7 @@
<path <path
id="path2991-7-1-4-1" id="path2991-7-1-4-1"
transform="translate(-154.10522,1432.0357)" transform="translate(-154.10522,1432.0357)"
d="m 359.99999,290.93362 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 z" d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571" sodipodi:ry="104.28571"
sodipodi:rx="104.28571" sodipodi:rx="104.28571"
sodipodi:cy="290.93362" sodipodi:cy="290.93362"
@@ -1592,7 +1592,7 @@
sodipodi:type="arc" /> sodipodi:type="arc" />
<path <path
id="path2993-4-5-8-7" id="path2993-4-5-8-7"
d="m 359.99999,290.93362 a 104.28571,104.28571 0 1 1 -208.57142,0 104.28571,104.28571 0 1 1 208.57142,0 z" d="m 359.99999,290.93362 c 0,57.59541 -46.6903,104.28572 -104.28571,104.28572 -57.59541,0 -104.28571,-46.69031 -104.28571,-104.28572 0,-57.5954 46.6903,-104.28571 104.28571,-104.28571 57.59541,0 104.28571,46.69031 104.28571,104.28571 z"
sodipodi:ry="104.28571" sodipodi:ry="104.28571"
sodipodi:rx="104.28571" sodipodi:rx="104.28571"
sodipodi:cy="290.93362" sodipodi:cy="290.93362"
@@ -1643,7 +1643,7 @@
sodipodi:cy="812.36218" sodipodi:cy="812.36218"
sodipodi:rx="10" sodipodi:rx="10"
sodipodi:ry="10" sodipodi:ry="10"
d="m 710,812.36218 a 10,10 0 1 1 -20,0 10,10 0 1 1 20,0 z" d="m 710,812.36218 c 0,5.52285 -4.47715,10 -10,10 -5.52285,0 -10,-4.47715 -10,-10 0,-5.52284 4.47715,-10 10,-10 5.52285,0 10,4.47716 10,10 z"
transform="matrix(1.2362333,-1.2362333,1.2362333,1.2362333,-1490.7493,1534.7336)" /> transform="matrix(1.2362333,-1.2362333,1.2362333,1.2362333,-1490.7493,1534.7336)" />
<rect <rect
style="fill:#ffffff;fill-opacity:1;stroke:#bebeff;stroke-width:10.80681515000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" style="fill:#ffffff;fill-opacity:1;stroke:#bebeff;stroke-width:10.80681515000000000;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
@@ -1670,7 +1670,7 @@
style="fill:#ffffff;stroke:#bebefa;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> style="fill:#ffffff;stroke:#bebefa;stroke-width:22.72570610000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path <path
transform="matrix(1.0049237,0,0,0.61497516,302.39116,1664.7945)" transform="matrix(1.0049237,0,0,0.61497516,302.39116,1664.7945)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z" d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915" sodipodi:ry="35.140915"
sodipodi:rx="21.718279" sodipodi:rx="21.718279"
sodipodi:cy="230.89374" sodipodi:cy="230.89374"
@@ -1680,7 +1680,7 @@
sodipodi:type="arc" /> sodipodi:type="arc" />
<path <path
transform="matrix(1.0049237,0,0,0.61497516,300.85563,1514.4712)" transform="matrix(1.0049237,0,0,0.61497516,300.85563,1514.4712)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z" d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915" sodipodi:ry="35.140915"
sodipodi:rx="21.718279" sodipodi:rx="21.718279"
sodipodi:cy="230.89374" sodipodi:cy="230.89374"
@@ -1690,7 +1690,7 @@
sodipodi:type="arc" /> sodipodi:type="arc" />
<path <path
transform="matrix(1.0049237,0,0,0.61497516,401.70879,1561.5007)" transform="matrix(1.0049237,0,0,0.61497516,401.70879,1561.5007)"
d="m 372.74629,230.89374 a 21.718279,35.140915 0 1 1 -43.43655,0 21.718279,35.140915 0 1 1 43.43655,0 z" d="m 372.74629,230.89374 c 0,19.40779 -9.7236,35.14091 -21.71827,35.14091 -11.99468,0 -21.71828,-15.73312 -21.71828,-35.14091 0,-19.40779 9.7236,-35.14092 21.71828,-35.14092 11.99467,0 21.71827,15.73313 21.71827,35.14092 z"
sodipodi:ry="35.140915" sodipodi:ry="35.140915"
sodipodi:rx="21.718279" sodipodi:rx="21.718279"
sodipodi:cy="230.89374" sodipodi:cy="230.89374"
@@ -1698,6 +1698,147 @@
id="path3795-8-4-8-2-1" id="path3795-8-4-8-2-1"
style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" style="fill:#ffffff;stroke:#bebeff;stroke-width:12.04511166000000000;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
sodipodi:type="arc" /> sodipodi:type="arc" />
<g
id="g3992">
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
style="fill:#3c3c3c;fill-opacity:1;stroke:none"
width="34.635483"
height="158.96587"
x="1836.6243"
y="-1788.4895"
id="rect2995-0-8-4-1" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
style="fill:#3c3c3c;fill-opacity:1;stroke:none"
width="33.538391"
height="96.944809"
x="1628.6003"
y="1772.8655"
id="rect2995-0-8-4-1-4" />
</g>
<g
id="g4112"
transform="translate(88.611046,-13.773858)">
<rect
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)"
style="fill:#a0a0a0;fill-opacity:1;stroke:none"
width="34.635483"
height="158.96587"
x="1527.2657"
y="-1466.7803"
id="rect2995-0-8-4-1-5" />
<rect
transform="matrix(0.70710678,-0.70710678,0.70710678,0.70710678,0,0)"
style="fill:#a0a0a0;fill-opacity:1;stroke:none"
width="33.538391"
height="96.944809"
x="1306.8911"
y="1463.507"
id="rect2995-0-8-4-1-4-5" />
</g>
<path
style="fill:#b3b3b3;stroke:none"
d="m 2185.2705,373.3859 -109.47,85.45235 29.4727,-89.94984 z"
id="path3894-1-1"
inkscape:connector-curvature="0" />
<rect
style="fill:#b3b3b3;stroke:#b3b3b3;stroke-width:49.97417831;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0"
id="rect3088-5-5-7"
width="174.36192"
height="89.170021"
x="2060.0393"
y="293.00055" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#dcdcdc;stroke-width:2.10925268999999990;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4170"
width="35.913948"
height="206.36755"
x="2110.2112"
y="507.8555" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#ffffff;stroke-width:15.12008381000000100;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4166"
width="174.5864"
height="76.446434"
x="2035.1414"
y="548.66016" />
<rect
style="fill:#dcdcdc;fill-opacity:1;stroke:#dcdcdc;stroke-width:1.42725468000000010;stroke-linejoin:miter;stroke-miterlimit:4.30000019000000040;stroke-opacity:1;stroke-dasharray:none"
id="rect4174"
width="43.442127"
height="43.442127"
x="1928.0846"
y="-1122.7543"
transform="matrix(0.72181305,0.69208809,-0.72181305,0.69208809,0,0)" />
<path
sodipodi:type="arc"
style="fill:#ffffe6;fill-opacity:1;stroke:#ffffff;stroke-width:10.1960001;stroke-linejoin:miter;stroke-miterlimit:4.30000019;stroke-opacity:1;stroke-dasharray:none"
id="path4364"
sodipodi:cx="1418.2542"
sodipodi:cy="434.14883"
sodipodi:rx="11.111678"
sodipodi:ry="11.111678"
d="m 1429.3658,434.14883 c 0,6.13681 -4.9748,11.11168 -11.1116,11.11168 -6.1369,0 -11.1117,-4.97487 -11.1117,-11.11168 0,-6.13681 4.9748,-11.11167 11.1117,-11.11167 6.1368,0 11.1116,4.97486 11.1116,11.11167 z"
transform="matrix(1.2783369,0,0,1.2783369,315.0834,31.171302)" />
<path
style="fill:#dcdcdc;stroke:none;fill-opacity:1"
d="m 2533.6893,373.6989 -109.47,85.45235 29.4727,-89.94984 z"
id="path3894-1-1-1"
inkscape:connector-curvature="0" />
<rect
style="fill:#dcdcdc;stroke:#dcdcdc;stroke-width:49.97417449999999700;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;fill-opacity:1"
id="rect3088-5-5-7-7"
width="174.36192"
height="89.170021"
x="2408.458"
y="293.31354" />
<rect
style="fill:#3c3c3c;fill-opacity:1;stroke:#888888;stroke-width:73.08132935000000400;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3220"
width="104.54597"
height="104.54597"
x="45.94949"
y="1925.303" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1"
width="117.84303"
height="30.608574"
x="1271.0641"
y="-1484.6459"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-7"
width="117.84303"
height="30.608574"
x="1408.8896"
y="1314.712"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
<rect
style="fill:#0088cc;fill-opacity:1;stroke:#0088cc;stroke-width:73.08132935000000400;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none"
id="rect3220-4"
width="104.54597"
height="104.54597"
x="337.49615"
y="1924.5376" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-0"
width="117.84303"
height="30.608574"
x="1064.3683"
y="-1690.2594"
transform="matrix(-0.70710678,0.70710678,-0.70710678,-0.70710678,0,0)" />
<rect
style="fill:#ffffff;fill-opacity:1;stroke:none"
id="rect3998-1-7-9"
width="117.84303"
height="30.608574"
x="1614.5032"
y="1108.0162"
transform="matrix(0.70710678,0.70710678,-0.70710678,0.70710678,0,0)" />
</g> </g>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -50,20 +50,20 @@ trait DashboardControllerBase extends ControllerBase {
val userName = context.loginAccount.get.userName val userName = context.loginAccount.get.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 filterUser = Map(filter -> userName)
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
dashboard.html.issues( dashboard.html.issues(
issues.html.listparts( dashboard.html.issueslist(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, userRepos: _*), searchIssue(condition, 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), condition),
countIssue(condition, Map.empty, false, userRepos: _*), countIssue(condition.copy(assigned = None, author = None), false, userRepos: _*),
countIssue(condition, Map("assigned" -> userName), false, userRepos: _*), countIssue(condition.copy(assigned = Some(userName), author = None), false, userRepos: _*),
countIssue(condition, Map("created_by" -> userName), false, userRepos: _*), countIssue(condition.copy(assigned = None, author = Some(userName)), false, userRepos: _*),
countIssueGroupByRepository(condition, filterUser, false, userRepos: _*), countIssueGroupByRepository(condition, false, userRepos: _*),
condition, condition,
filter) filter)
@@ -86,14 +86,14 @@ trait DashboardControllerBase extends ControllerBase {
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val counts = countIssueGroupByRepository( val counts = countIssueGroupByRepository(
IssueSearchCondition().copy(state = condition.state), Map.empty, true, userRepos: _*) IssueSearchCondition().copy(state = condition.state), true, userRepos: _*)
dashboard.html.pulls( dashboard.html.pulls(
pulls.html.listparts( dashboard.html.pullslist(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, allRepos: _*), searchIssue(condition, 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, condition,
None, None,
false), false),

View File

@@ -21,7 +21,6 @@ trait IssuesControllerBase extends ControllerBase {
case class IssueCreateForm(title: String, content: Option[String], case class IssueCreateForm(title: String, content: Option[String],
assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String])
case class IssueEditForm(title: String, content: Option[String])
case class CommentForm(issueId: Int, content: String) case class CommentForm(issueId: Int, content: String)
case class IssueStateForm(issueId: Int, content: Option[String]) case class IssueStateForm(issueId: Int, content: Option[String])
@@ -33,10 +32,12 @@ trait IssuesControllerBase extends ControllerBase {
"labelNames" -> trim(optional(text())) "labelNames" -> trim(optional(text()))
)(IssueCreateForm.apply) )(IssueCreateForm.apply)
val issueTitleEditForm = mapping(
"title" -> trim(label("Title", text(required)))
)(x => x)
val issueEditForm = mapping( val issueEditForm = mapping(
"title" -> trim(label("Title", text(required))), "content" -> trim(optional(text()))
"content" -> trim(optional(text())) )(x => x)
)(IssueEditForm.apply)
val commentForm = mapping( val commentForm = mapping(
"issueId" -> label("Issue Id", number()), "issueId" -> label("Issue Id", number()),
@@ -48,16 +49,8 @@ trait IssuesControllerBase extends ControllerBase {
"content" -> trim(optional(text())) "content" -> trim(optional(text()))
)(IssueStateForm.apply) )(IssueStateForm.apply)
get("/:owner/:repository/issues")(referrersOnly { get("/:owner/:repository/issues")(referrersOnly { repository =>
searchIssues("all", _) searchIssues(repository)
})
get("/:owner/:repository/issues/assigned/:userName")(referrersOnly {
searchIssues("assigned", _)
})
get("/:owner/:repository/issues/created_by/:userName")(referrersOnly {
searchIssues("created_by", _)
}) })
get("/:owner/:repository/issues/:id")(referrersOnly { repository => get("/:owner/:repository/issues/:id")(referrersOnly { repository =>
@@ -126,14 +119,29 @@ trait IssuesControllerBase extends ControllerBase {
} }
}) })
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/edit_title/:id", issueTitleEditForm)(readableUsersOnly { (title, repository) =>
defining(repository.owner, repository.name){ case (owner, name) => defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue => getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){ if(isEditable(owner, name, issue.openedUserName)){
// update issue // update issue
updateIssue(owner, name, issue.issueId, form.title, form.content) updateIssue(owner, name, issue.issueId, title, issue.content)
// extract references and create refer comment // extract references and create refer comment
createReferComment(owner, name, issue, form.title + " " + form.content.getOrElse("")) createReferComment(owner, name, issue.copy(title = title), title)
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized
} getOrElse NotFound
}
})
ajaxPost("/:owner/:repository/issues/edit/:id", issueEditForm)(readableUsersOnly { (content, repository) =>
defining(repository.owner, repository.name){ case (owner, name) =>
getIssue(owner, name, params("id")).map { issue =>
if(isEditable(owner, name, issue.openedUserName)){
// update issue
updateIssue(owner, name, issue.issueId, issue.title, content)
// extract references and create refer comment
createReferComment(owner, name, issue, content.getOrElse(""))
redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}") redirect(s"/${owner}/${name}/issues/_data/${issue.issueId}")
} else Unauthorized } else Unauthorized
@@ -181,7 +189,7 @@ trait IssuesControllerBase extends ControllerBase {
if(isEditable(x.userName, x.repositoryName, x.openedUserName)){ if(isEditable(x.userName, x.repositoryName, x.openedUserName)){
params.get("dataType") collect { params.get("dataType") collect {
case t if t == "html" => issues.html.editissue( case t if t == "html" => issues.html.editissue(
x.title, x.content, x.issueId, x.userName, x.repositoryName) x.content, x.issueId, x.userName, x.repositoryName)
} getOrElse { } getOrElse {
contentType = formats("json") contentType = formats("json")
org.json4s.jackson.Serialization.write( org.json4s.jackson.Serialization.write(
@@ -235,15 +243,17 @@ trait IssuesControllerBase extends ControllerBase {
milestoneId("milestoneId").map { milestoneId => milestoneId("milestoneId").map { milestoneId =>
getMilestonesWithIssueCount(repository.owner, repository.name) getMilestonesWithIssueCount(repository.owner, repository.name)
.find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) => .find(_._1.milestoneId == milestoneId).map { case (_, openCount, closeCount) =>
issues.milestones.html.progress(openCount + closeCount, closeCount, false) issues.milestones.html.progress(openCount + closeCount, closeCount)
} getOrElse NotFound } getOrElse NotFound
} getOrElse Ok() } getOrElse Ok()
}) })
post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository => post("/:owner/:repository/issues/batchedit/state")(collaboratorsOnly { repository =>
defining(params.get("value")){ action => defining(params.get("value")){ action =>
executeBatch(repository) { action match {
handleComment(_, None, repository)( _ => action) case Some("open") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("reopen")) }
case Some("close") => executeBatch(repository) { handleComment(_, None, repository)( _ => Some("close")) }
case _ => // TODO BadRequest
} }
} }
}) })
@@ -293,7 +303,10 @@ trait IssuesControllerBase extends ControllerBase {
private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = {
params("checked").split(',') map(_.toInt) foreach execute params("checked").split(',') map(_.toInt) foreach execute
redirect(s"/${repository.owner}/${repository.name}/issues") params("from") match {
case "issues" => redirect(s"/${repository.owner}/${repository.name}/issues")
case "pulls" => redirect(s"/${repository.owner}/${repository.name}/pulls")
}
} }
private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = { private def createReferComment(owner: String, repository: String, fromIssue: Issue, message: String) = {
@@ -319,15 +332,15 @@ trait IssuesControllerBase extends ControllerBase {
val (action, recordActivity) = val (action, recordActivity) =
getAction(issue) getAction(issue)
.collect { .collect {
case "close" => true -> (Some("close") -> case "close" if(!issue.closed) => true ->
Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _)) (Some("close") -> Some(if(issue.isPullRequest) recordClosePullRequestActivity _ else recordCloseIssueActivity _))
case "reopen" => false -> (Some("reopen") -> case "reopen" if(issue.closed) => false ->
Some(recordReopenIssueActivity _)) (Some("reopen") -> Some(recordReopenIssueActivity _))
} }
.map { case (closed, t) => .map { case (closed, t) =>
updateClosed(owner, name, issueId, closed) updateClosed(owner, name, issueId, closed)
t t
} }
.getOrElse(None -> None) .getOrElse(None -> None)
val commentId = content val commentId = content
@@ -337,7 +350,7 @@ trait IssuesControllerBase extends ControllerBase {
case (content, action) => createComment(owner, name, userName, issueId, content, action) case (content, action) => createComment(owner, name, userName, issueId, content, action)
} }
// record activity // record comment activity if comment is entered
content foreach { content foreach {
(if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _) (if(issue.isPullRequest) recordCommentPullRequestActivity _ else recordCommentIssueActivity _)
(owner, name, userName, issueId, _) (owner, name, userName, issueId, _)
@@ -370,9 +383,8 @@ trait IssuesControllerBase extends ControllerBase {
} }
} }
private def searchIssues(filter: String, repository: RepositoryService.RepositoryInfo) = { private def searchIssues(repository: RepositoryService.RepositoryInfo) = {
defining(repository.owner, repository.name){ case (owner, repoName) => defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = Map(filter -> params.getOrElse("userName", ""))
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Issues(owner, repoName) val sessionKey = Keys.Session.Issues(owner, repoName)
@@ -383,19 +395,15 @@ trait IssuesControllerBase extends ControllerBase {
) )
issues.html.list( issues.html.list(
searchIssue(condition, filterUser, false, (page - 1) * IssueLimit, IssueLimit, owner -> repoName), "issues",
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"), filterUser, false, owner -> repoName), countIssue(condition.copy(state = "open" ), false, owner -> repoName),
countIssue(condition.copy(state = "closed"), filterUser, false, owner -> repoName), countIssue(condition.copy(state = "closed"), false, owner -> repoName),
countIssue(condition, Map.empty, false, owner -> repoName),
context.loginAccount.map(x => countIssue(condition, Map("assigned" -> x.userName), false, owner -> repoName)),
context.loginAccount.map(x => countIssue(condition, Map("created_by" -> x.userName), false, owner -> repoName)),
countIssueGroupByLabels(owner, repoName, condition, filterUser),
condition, condition,
filter,
repository, repository,
hasWritePermission(owner, repoName, context.loginAccount)) hasWritePermission(owner, repoName, context.loginAccount))
} }

View File

@@ -2,51 +2,67 @@ package app
import jp.sf.amateras.scalatra.forms._ import jp.sf.amateras.scalatra.forms._
import service._ import service._
import util.CollaboratorsAuthenticator import util.{ReferrerAuthenticator, CollaboratorsAuthenticator}
import util.Implicits._ import util.Implicits._
import org.scalatra.i18n.Messages import org.scalatra.i18n.Messages
import org.scalatra.Ok
class LabelsController extends LabelsControllerBase class LabelsController extends LabelsControllerBase
with LabelsService with RepositoryService with AccountService with CollaboratorsAuthenticator with LabelsService with IssuesService with RepositoryService with AccountService
with ReferrerAuthenticator with CollaboratorsAuthenticator
trait LabelsControllerBase extends ControllerBase { trait LabelsControllerBase extends ControllerBase {
self: LabelsService with RepositoryService with CollaboratorsAuthenticator => self: LabelsService with IssuesService with RepositoryService
with ReferrerAuthenticator with CollaboratorsAuthenticator =>
case class LabelForm(labelName: String, color: String) case class LabelForm(labelName: String, color: String)
val newForm = mapping( val labelForm = mapping(
"newLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), "labelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))),
"newColor" -> trim(label("Color", text(required, color))) "labelColor" -> trim(label("Color", text(required, color)))
)(LabelForm.apply) )(LabelForm.apply)
val editForm = mapping( get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
"editLabelName" -> trim(label("Label name", text(required, labelName, maxlength(100)))), issues.labels.html.list(
"editColor" -> trim(label("Color", text(required, color))) getLabels(repository.owner, repository.name),
)(LabelForm.apply) countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
post("/:owner/:repository/issues/label/new", newForm)(collaboratorsOnly { (form, repository) => hasWritePermission(repository.owner, repository.name, context.loginAccount))
createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
redirect(s"/${repository.owner}/${repository.name}/issues")
}) })
ajaxGet("/:owner/:repository/issues/label/edit")(collaboratorsOnly { repository => ajaxGet("/:owner/:repository/issues/labels/new")(collaboratorsOnly { repository =>
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) issues.labels.html.edit(None, repository)
}) })
ajaxGet("/:owner/:repository/issues/label/:labelId/edit")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/labels/new", labelForm)(collaboratorsOnly { (form, repository) =>
val labelId = createLabel(repository.owner, repository.name, form.labelName, form.color.substring(1))
issues.labels.html.label(
getLabel(repository.owner, repository.name, labelId).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
})
ajaxGet("/:owner/:repository/issues/labels/:labelId/edit")(collaboratorsOnly { repository =>
getLabel(repository.owner, repository.name, params("labelId").toInt).map { label => getLabel(repository.owner, repository.name, params("labelId").toInt).map { label =>
issues.labels.html.edit(Some(label), repository) issues.labels.html.edit(Some(label), repository)
} getOrElse NotFound() } getOrElse NotFound()
}) })
ajaxPost("/:owner/:repository/issues/label/:labelId/edit", editForm)(collaboratorsOnly { (form, repository) => ajaxPost("/:owner/:repository/issues/labels/:labelId/edit", labelForm)(collaboratorsOnly { (form, repository) =>
updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1)) updateLabel(repository.owner, repository.name, params("labelId").toInt, form.labelName, form.color.substring(1))
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) issues.labels.html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
hasWritePermission(repository.owner, repository.name, context.loginAccount))
}) })
ajaxGet("/:owner/:repository/issues/label/:labelId/delete")(collaboratorsOnly { repository => ajaxPost("/:owner/:repository/issues/labels/:labelId/delete")(collaboratorsOnly { repository =>
deleteLabel(repository.owner, repository.name, params("labelId").toInt) deleteLabel(repository.owner, repository.name, params("labelId").toInt)
issues.labels.html.editlist(getLabels(repository.owner, repository.name), repository) Ok()
}) })
/** /**

View File

@@ -62,10 +62,6 @@ trait PullRequestsControllerBase extends ControllerBase {
searchPullRequests(None, repository) searchPullRequests(None, repository)
}) })
get("/:owner/:repository/pulls/:userName")(referrersOnly { repository =>
searchPullRequests(Some(params("userName")), repository)
})
get("/:owner/:repository/pull/:id")(referrersOnly { repository => get("/:owner/:repository/pull/:id")(referrersOnly { repository =>
params("id").toIntOpt.flatMap{ issueId => params("id").toIntOpt.flatMap{ issueId =>
val owner = repository.owner val owner = repository.owner
@@ -453,7 +449,6 @@ trait PullRequestsControllerBase extends ControllerBase {
private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) = private def searchPullRequests(userName: Option[String], repository: RepositoryService.RepositoryInfo) =
defining(repository.owner, repository.name){ case (owner, repoName) => defining(repository.owner, repository.name){ case (owner, repoName) =>
val filterUser = userName.map { x => Map("created_by" -> x) } getOrElse Map("all" -> "")
val page = IssueSearchCondition.page(request) val page = IssueSearchCondition.page(request)
val sessionKey = Keys.Session.Pulls(owner, repoName) val sessionKey = Keys.Session.Pulls(owner, repoName)
@@ -463,14 +458,15 @@ trait PullRequestsControllerBase extends ControllerBase {
else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition()) else session.getAs[IssueSearchCondition](sessionKey).getOrElse(IssueSearchCondition())
) )
pulls.html.list( issues.html.list(
searchIssue(condition, filterUser, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName), "pulls",
getPullRequestCountGroupByUser(condition.state == "closed", Some(owner), Some(repoName)), searchIssue(condition, true, (page - 1) * PullRequestLimit, PullRequestLimit, owner -> repoName),
userName,
page, page,
countIssue(condition.copy(state = "open" ), filterUser, true, owner -> repoName), (getCollaborators(owner, repoName) :+ owner).sorted,
countIssue(condition.copy(state = "closed"), filterUser, true, owner -> repoName), getMilestones(owner, repoName),
countIssue(condition, Map.empty, true, owner -> repoName), getLabels(owner, repoName),
countIssue(condition.copy(state = "open" ), 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

@@ -31,7 +31,7 @@ case class Label(
if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){ if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){
"000000" "000000"
} else { } else {
"FFFFFF" "ffffff"
} }
} }
} }

View File

@@ -43,14 +43,13 @@ trait IssuesService {
* Returns the count of the search result against issues. * Returns the count of 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" or "created_by", value is the user name)
* @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request. * @param onlyPullRequest if true then counts only pull request, false then counts both of issue and pull request.
* @param repos Tuple of the repository owner and the repository name * @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)*)
repos: (String, String)*)(implicit s: Session): Int = (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.
@@ -58,13 +57,12 @@ trait IssuesService {
* @param owner the repository owner * @param owner the repository owner
* @param repository the repository name * @param repository the repository name
* @param condition the search condition * @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @return the Map which contains issue count for each labels (key is label name, value is issue count) * @return the Map which contains issue count for each labels (key is label name, value is issue count)
*/ */
def countIssueGroupByLabels(owner: String, repository: String, condition: IssueSearchCondition, def countIssueGroupByLabels(owner: String, repository: String,
filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = { condition: IssueSearchCondition)(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)
} }
@@ -84,15 +82,14 @@ trait IssuesService {
* If the issue does not exist, its repository is not included in the result. * If the issue does not exist, its repository is not included in the result.
* *
* @param condition the search condition * @param condition the search condition
* @param filterUser the filter user name (key is "all", "assigned" or "created_by", value is the user name)
* @param onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request. * @param 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 * @param repos Tuple of the repository owner and the repository name
* @return list which contains issue count for each repository * @return list which contains issue count for each repository
*/ */
def countIssueGroupByRepository( def countIssueGroupByRepository(
condition: IssueSearchCondition, filterUser: Map[String, String], onlyPullRequest: Boolean, condition: IssueSearchCondition, onlyPullRequest: Boolean,
repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = { repos: (String, String)*)(implicit s: Session): List[(String, String, Int)] = {
searchIssueQuery(repos, condition.copy(repo = None), filterUser, onlyPullRequest) searchIssueQuery(repos, condition.copy(repo = None), onlyPullRequest)
.groupBy { t => .groupBy { t =>
t.userName -> t.repositoryName t.userName -> t.repositoryName
} }
@@ -107,19 +104,18 @@ trait IssuesService {
* 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 onlyPullRequest if true then returns only pull request, false then returns both of issue and pull request.
* @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], onlyPullRequest: 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[(Issue, List[Label], Int)] = { (implicit s: Session): List[IssueInfo] = {
// get issues and comment count and labels // get issues and comment count and labels
searchIssueQuery(repos, condition, filterUser, onlyPullRequest) searchIssueQuery(repos, condition, pullRequest)
.innerJoin(IssueOutline).on { (t1, t2) => t1.byIssue(t2.userName, t2.repositoryName, t2.issueId) } .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 {
@@ -136,21 +132,23 @@ trait IssuesService {
.drop(offset).take(limit) .drop(offset).take(limit)
.leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } .leftJoin (IssueLabels) .on { case ((t1, t2), t3) => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) }
.leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) } .leftJoin (Labels) .on { case (((t1, t2), t3), t4) => t3.byLabel(t4.userName, t4.repositoryName, t4.labelId) }
.map { case (((t1, t2), t3), t4) => .leftJoin (Milestones) .on { case ((((t1, t2), t3), t4), t5) => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) }
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?) .map { case ((((t1, t2), t3), t4), t5) =>
(t1, t2.commentCount, t4.labelId.?, t4.labelName.?, t4.color.?, t5.title.?)
} }
.list .list
.splitWith { (c1, c2) => .splitWith { (c1, c2) =>
c1._1.userName == c2._1.userName && c1._1.userName == c2._1.userName &&
c1._1.repositoryName == c2._1.repositoryName && c1._1.repositoryName == c2._1.repositoryName &&
c1._1.issueId == c2._1.issueId c1._1.issueId == c2._1.issueId
} }
.map { issues => issues.head match { .map { issues => issues.head match {
case (issue, commentCount, _,_,_) => case (issue, commentCount, _, _, _, milestone) =>
(issue, IssueInfo(issue,
issues.flatMap { t => t._3.map ( issues.flatMap { t => t._3.map (
Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get) Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get)
)} toList, )} toList,
milestone,
commentCount) commentCount)
}} toList }} toList
} }
@@ -159,7 +157,7 @@ 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,
filterUser: Map[String, String], onlyPullRequest: Boolean)(implicit s: Session) = pullRequest: Boolean)(implicit s: Session) =
Issues filter { t1 => Issues filter { t1 =>
condition.repo condition.repo
.map { _.split('/') match { case array => Seq(array(0) -> array(1)) } } .map { _.split('/') match { case array => Seq(array(0) -> array(1)) } }
@@ -169,10 +167,9 @@ trait IssuesService {
(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.assignedUserName === condition.assigned.get.bind, condition.assigned.isDefined) &&
(t1.openedUserName === filterUser("created_by").bind, filterUser.get("created_by").isDefined) && (t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(t1.openedUserName =!= filterUser("not_created_by").bind, filterUser.get("not_created_by").isDefined) && (t1.pullRequest === pullRequest.bind) &&
(t1.pullRequest === true.bind, onlyPullRequest) &&
(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
@@ -337,11 +334,20 @@ object IssuesService {
case class IssueSearchCondition( case class IssueSearchCondition(
labels: Set[String] = Set.empty, labels: Set[String] = Set.empty,
milestoneId: Option[Option[Int]] = None, milestoneId: Option[Option[Int]] = None,
author: Option[String] = None,
assigned: Option[String] = None,
repo: Option[String] = None, repo: Option[String] = None,
state: String = "open", state: String = "open",
sort: String = "created", sort: String = "created",
direction: String = "desc"){ direction: String = "desc"){
def isEmpty: Boolean = {
labels.isEmpty && milestoneId.isEmpty && author.isEmpty && assigned.isEmpty &&
state == "open" && sort == "created" && direction == "desc"
}
def nonEmpty: Boolean = !isEmpty
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(","))),
@@ -349,6 +355,8 @@ object IssuesService {
case Some(x) => x.toString case Some(x) => x.toString
case None => "none" case None => "none"
})}, })},
author .map(x => "author=" + urlEncode(x)),
assigned.map(x => "assigned=" + urlEncode(x)),
repo.map("for=" + urlEncode(_)), repo.map("for=" + urlEncode(_)),
Some("state=" + urlEncode(state)), Some("state=" + urlEncode(state)),
Some("sort=" + urlEncode(sort)), Some("sort=" + urlEncode(sort)),
@@ -370,6 +378,8 @@ object IssuesService {
case "none" => None case "none" => None
case x => x.toIntOpt case x => x.toIntOpt
}, },
param(request, "author"),
param(request, "assigned"),
param(request, "for"), param(request, "for"),
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"),
@@ -383,4 +393,6 @@ object IssuesService {
} }
} }
case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int)
} }

View File

@@ -12,8 +12,8 @@ trait LabelsService {
def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] = def getLabel(owner: String, repository: String, labelId: Int)(implicit s: Session): Option[Label] =
Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption Labels.filter(_.byPrimaryKey(owner, repository, labelId)).firstOption
def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Unit = def createLabel(owner: String, repository: String, labelName: String, color: String)(implicit s: Session): Int =
Labels insert Label( Labels returning Labels.map(_.labelId) += Label(
userName = owner, userName = owner,
repositoryName = repository, repositoryName = repository,
labelName = labelName, labelName = labelName,

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

@@ -0,0 +1,184 @@
@(issues: List[service.IssuesService.IssueInfo],
page: Int,
openCount: Int,
closedCount: Int,
condition: service.IssuesService.IssueSearchCondition,
collaborators: List[String] = Nil,
milestones: List[model.Milestone] = Nil,
labels: List[model.Label] = Nil,
repository: Option[service.RepositoryService.RepositoryInfo] = None,
hasWritePermission: Boolean = false)(implicit context: app.Context)
@import context._
@import view.helpers._
@import service.IssuesService.IssueInfo
<div class="span9">
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear milestone and label filters
</a>
}
@if(condition.repo.isDefined){
<a href="@condition.copy(repo = None).toURL" id="clear-filter">
<i class="icon-remove-circle"></i> Clear filter on @condition.repo
</a>
}
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL)
</div>
<div class="btn-group">
<a class="btn btn-small@if(condition.state == "open"){ active}" href="@condition.copy(state = "open").toURL">@openCount Open</a>
<a class="btn btn-small@if(condition.state == "closed"){ active}" href="@condition.copy(state = "closed").toURL">@closedCount Closed</a>
</div>
@helper.html.dropdown(
value = (condition.sort, condition.direction) match {
case ("created" , "desc") => "Newest"
case ("created" , "asc" ) => "Oldest"
case ("comments", "desc") => "Most commented"
case ("comments", "asc" ) => "Least commented"
case ("updated" , "desc") => "Recently updated"
case ("updated" , "asc" ) => "Least recently updated"
},
prefix = "Sort",
mini = false
){
<li>
<a href="@condition.copy(sort="created", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
</a>
</li>
<li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
</a>
</li>
<li>
<a href="@condition.copy(sort="comments", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="desc").toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
</a>
</li>
<li>
<a href="@condition.copy(sort="updated", direction="asc" ).toURL">
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</a>
</li>
}
<table class="table table-bordered table-hover table-issues">
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No issues to show.
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
} else {
@if(repository.isDefined){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
}
}
</td>
</tr>
} else {
@if(hasWritePermission){
<tr>
<td style="background-color: #eee;">
<div class="btn-group">
<button class="btn btn-mini strong" id="state">@{if(condition.state == "open") "Close" else "Reopen"}</button>
</div>
@helper.html.dropdown("Label") {
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
<i class="icon-white"></i>
<span class="label" style="background-color: #@label.color;">&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) =>
<tr>
<td>
@if(hasWritePermission){
<label class="checkbox" style="cursor: default;">
<input type="checkbox" value="@issue.issueId"/>
}
@if(issue.isPullRequest){
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
} else {
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
}
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
@if(issue.isPullRequest){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
}
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
#@issue.issueId
</span>
<div class="small muted" style="margin-left: 20px;">
Opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
@if(hasWritePermission){
</label>
}
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>
</div>

View File

@@ -1,4 +1,4 @@
@(issues: List[(model.Issue, List[model.Label], Int)], @(issues: List[service.IssuesService.IssueInfo],
page: Int, page: Int,
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
@@ -7,6 +7,7 @@
hasWritePermission: Boolean)(implicit context: app.Context) hasWritePermission: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import service.IssuesService.IssueInfo
<div class="span9"> <div class="span9">
@repository.map { repository => @repository.map { repository =>
@if(hasWritePermission){ @if(hasWritePermission){
@@ -71,7 +72,7 @@
</td> </td>
</tr> </tr>
} }
@issues.map { case (issue, labels, commentCount) => @issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr> <tr>
<td> <td>
<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"/>

View File

@@ -1,6 +1,13 @@
@(value: String = "", prefix: String = "", mini: Boolean = true, style: String = "", right: Boolean = false)(body: Html) @(value : String = "",
<div class="btn-group"@if(style.nonEmpty){ style="@style"}> prefix: String = "",
<button class="btn dropdown-toggle@if(mini){ btn-mini} else { btn-small}" data-toggle="dropdown"> mini : Boolean = true,
style : String = "",
right : Boolean = false,
flat : Boolean = false)(body: Html)
<div class="btn-group" @if(style.nonEmpty){style="@style"}>
<button
@if(flat){style="border: none; background-color: #eee;"}
class="dropdown-toggle @if(!flat){btn} else {flat} @if(mini){btn-mini} else {btn-small}" data-toggle="dropdown">
@if(value.isEmpty){ @if(value.isEmpty){
<i class="icon-cog"></i> <i class="icon-cog"></i>
} else { } else {

View File

@@ -5,6 +5,7 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@if(loginAccount.isDefined){ @if(loginAccount.isDefined){
<hr/><br/>
<form method="POST" validate="true"> <form method="POST" validate="true">
<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">

View File

@@ -5,20 +5,36 @@
pullreq: Option[model.PullRequest] = None)(implicit context: app.Context) pullreq: Option[model.PullRequest] = None)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
<div class="box issue-comment-box">
<div class="box-header-small">
@user(issue.openedUserName, styleClass="username strong") <span class="muted">commented on @datetime(issue.registeredDate)</span>
<span class="pull-right">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a href="#" data-issue-id="@issue.issueId"><i class="icon-pencil"></i></a>
}
</span>
</div>
<div class="box-content issue-content" id="issueContent">
@markdown(issue.content getOrElse "No description provided.", repository, false, true)
</div>
</div>
@comments.map { comment => @comments.map { comment =>
@if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){ @if(comment.action != "close" && comment.action != "reopen" && comment.action != "delete_branch"){
<div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div> <div class="issue-avatar-image">@avatar(comment.commentedUserName, 48)</div>
<div class="box issue-comment-box" id="comment-@comment.commentId"> <div class="box issue-comment-box" id="comment-@comment.commentId">
<div class="box-header-small"> <div class="box-header-small">
<i class="icon-comment"></i>
@user(comment.commentedUserName, styleClass="username strong") @user(comment.commentedUserName, styleClass="username strong")
@if(comment.action == "comment"){ <span class="muted">
commented @if(comment.action == "comment"){
} else { commented
@if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request } } else {
} @if(pullreq.isEmpty){ referenced the issue } else { referenced the pull request }
}
on @datetime(comment.registeredDate)
</span>
<span class="pull-right"> <span class="pull-right">
@datetime(comment.registeredDate)
@if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" && @if(comment.action != "commit" && comment.action != "merge" && comment.action != "refer" &&
(hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){ (hasWritePermission || loginAccount.map(_.userName == comment.commentedUserName).getOrElse(false))){
<a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp; <a href="#" data-comment-id="@comment.commentId"><i class="icon-pencil"></i></a>&nbsp;
@@ -86,13 +102,22 @@
<script> <script>
$(function(){ $(function(){
$('i.icon-pencil').click(function(){ $('i.icon-pencil').click(function(){
var id = $(this).closest('a').data('comment-id'); var id = $(this).closest('a').data('comment-id');
$.get('@url(repository)/issue_comments/_data/' + id, var url = '@url(repository)/issue_comments/_data/' + id;
var $content = $('#commentContent-' + id);
if(!id){
id = $(this).closest('a').data('issue-id');
url = '@url(repository)/issues/_data/' + id;
$content = $('#issueContent');
}
$.get(url,
{ {
dataType : 'html' dataType : 'html'
}, },
function(data){ function(data){
$('#commentContent-' + id).empty().html(data); $content.empty().html(data);
}); });
return false; return false;
}); });

View File

@@ -7,7 +7,8 @@
@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("", true, repository) @tab("issues", false, repository)
<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">
<div class="span9"> <div class="span9">
@@ -32,7 +33,7 @@
@if(hasWritePermission){ @if(hasWritePermission){
<input type="hidden" name="milestoneId" value=""/> <input type="hidden" name="milestoneId" value=""/>
@helper.html.dropdown() { @helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> No milestone</a></li> <li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_.closedDate.isEmpty).map { milestone => @milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li> <li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title"> <a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@@ -40,9 +41,9 @@
<div class="small" style="padding-left: 20px;"> <div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate => @milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){ @if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate) <img src="@assets/common/images/alert.png"/><span class="milestone-alert">Due by @date(dueDate)</span>
} else { } else {
<span class="muted">Due in @date(dueDate)</span> <span class="muted">Due by @date(dueDate)</span>
} }
}.getOrElse { }.getOrElse {
<span class="muted">No due date</span> <span class="muted">No due date</span>
@@ -65,7 +66,7 @@
</div> </div>
<div class="span3"> <div class="span3">
@if(hasWritePermission){ @if(hasWritePermission){
<span class="strong">Add Labels</span> <span class="strong">Labels</span>
<div> <div>
<div id="label-list"> <div id="label-list">
<ul class="label-list nav nav-pills nav-stacked"> <ul class="label-list nav nav-pills nav-stacked">
@@ -112,7 +113,7 @@ $(function(){
if(milestoneId == ''){ if(milestoneId == ''){
$('#label-milestone').text('No milestone'); $('#label-milestone').text('No milestone');
} else { } else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title))); $('#label-milestone').html($('<span class="strong">').text(title));
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok'); $('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
} }
$('input[name=milestoneId]').val(milestoneId); $('input[name=milestoneId]').val(milestoneId);

View File

@@ -5,8 +5,8 @@
<textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea> <textarea style="width: 635px; height: 100px;" id="edit-content-@commentId">@content</textarea>
} }
<div> <div>
<input type="button" id="update-comment-@commentId" class="btn btn-small" value="Update Comment"/> <input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger" value="Cancel"/>
<input type="button" id="cancel-comment-@commentId" class="btn btn-small btn-danger pull-right" value="Cancel"/> <input type="button" id="update-comment-@commentId" class="btn btn-small pull-right" value="Update comment"/>
</div> </div>
<script> <script>
$(function(){ $(function(){

View File

@@ -1,42 +1,35 @@
@(title: String, content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context) @(content: Option[String], issueId: Int, owner: String, repository: String)(implicit context: app.Context)
@import context._ @import context._
<span id="error-edit-title" class="error"></span>
<input type="text" style="width: 635px;" id="edit-title" value="@title"/>
@helper.html.attached(owner, repository){ @helper.html.attached(owner, repository){
<textarea style="width: 635px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea> <textarea style="width: 635px; height: 100px; max-height: 300px;" id="edit-content">@content.getOrElse("")</textarea>
} }
<div> <div>
<input type="button" id="update" class="btn btn-small" value="Update Issue"/> <input type="button" id="cancel-issue" class="btn btn-small btn-danger" value="Cancel"/>
<input type="button" id="cancel" class="btn btn-small btn-danger pull-right" value="Cancel"/> <input type="button" id="update-issue" class="btn btn-small pull-right" value="Update comment"/>
</div> </div>
<script> <script>
$(function(){ $(function(){
$('#edit-content').elastic();
var callback = function(data){ var callback = function(data){
$('#update, #cancel').removeAttr('disabled'); $('#update, #cancel').removeAttr('disabled');
$('#issueTitle').empty().text(data.title);
$('#issueContent').empty().html(data.content); $('#issueContent').empty().html(data.content);
}; };
$('#update').click(function(){ $('#update-issue').click(function(){
$('#update, #cancel').attr('disabled', 'disabled'); $('#update, #cancel').attr('disabled', 'disabled');
$.ajax({ $.ajax({
url: '@path/@owner/@repository/issues/edit/@issueId', url: '@path/@owner/@repository/issues/edit/@issueId',
type: 'POST', type: 'POST',
data: { data: {
title : $('#edit-title').val(),
content : $('#edit-content').val() content : $('#edit-content').val()
} }
}).done( }).done(
callback callback
).fail(function(req) { ).fail(function(req) {
$('#update, #cancel').removeAttr('disabled'); $('#update, #cancel').removeAttr('disabled');
$('#error-edit-title').text($.parseJSON(req.responseText).title);
}); });
}); });
$('#cancel').click(function(){ $('#cancel-issue').click(function(){
$('#update, #cancel').attr('disabled', 'disabled'); $('#update, #cancel').attr('disabled', 'disabled');
$.get('@path/@owner/@repository/issues/_data/@issueId', callback); $.get('@path/@owner/@repository/issues/_data/@issueId', callback);
return false; return false;

View File

@@ -10,31 +10,86 @@
@import view.helpers._ @import view.helpers._
@html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){ @html.main(s"${issue.title} - Issue #${issue.issueId} - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu("issues", repository){
@tab("issues", false, repository)
<ul class="nav nav-tabs pull-left fill-width"> <ul class="nav nav-tabs pull-left fill-width">
<li class="pull-left"><a href="@url(repository)/issues"><i class="icon-arrow-left"></i> Back to issue list</a></li> <li class="pull-left">
<li class="pull-right">Issue #@issue.issueId</li> <h1>
</ul> <span class="show-title">
<div class="row-fluid"> <span id="show-title">@issue.title</span>
<div class="span10"> <span class="muted">#@issue.issueId</span>
@issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository) </span>
@commentlist(issue, comments, hasWritePermission, repository) <span class="edit-title" style="display: none;">
@commentform(issue, true, hasWritePermission, repository) <span id="error-edit-title" class="error"></span>
</div> <input type="text" class="span9" id="edit-title" value="@issue.title"/>
<div class="span2"> </span>
</h1>
@if(issue.closed) { @if(issue.closed) {
<span class="label label-important issue-status">Closed</span> <span class="label label-important issue-status">Closed</span>
} else { } else {
<span class="label label-success issue-status">Open</span> <span class="label label-success issue-status">Open</span>
} }
<div class="small" style="text-align: center;"> <span class="muted">
@defining(comments.filter( _.action.contains("comment") ).size){ count => @user(issue.openedUserName, styleClass="username strong") opened this issue on @datetime(issue.registeredDate) - @defining(
<span class="strong">@count</span> @plural(count, "comment") comments.filter( _.action.contains("comment") ).size
){ count =>
@count @plural(count, "comment")
} }
</span>
<br/><br/>
</li>
<li class="pull-right">
<div class="show-title">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<a class="btn btn-small" href="#" id="edit">Edit</a>
}
<a class="btn btn-small btn-success" href="@url(repository)/issues/new">New issue</a>
</div> </div>
<hr/> <div class="edit-title" style="display: none;">
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository) <a class="btn" href="#" id="update">Save</a> <a href="#" id="cancel">Cancel</a>
</div>
</li>
</ul>
<div class="row-fluid">
<div class="span10">
@commentlist(issue, comments, hasWritePermission, repository)
@commentform(issue, true, hasWritePermission, repository)
</div>
<div class="span2">
@issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div> </div>
</div> </div>
} }
} }
<script>
$(function(){
$('#edit').click(function(){
$('.edit-title').show();
$('.show-title').hide();
return false;
});
$('#update').click(function(){
$(this).attr('disabled', 'disabled');
$.ajax({
url: '@url(repository)/issues/edit_title/@issue.issueId',
type: 'POST',
data: {
title : $('#edit-title').val()
}
}).done(function(data){
$('#show-title').empty().text(data.title);
$('#cancel').click();
$(this).removeAttr('disabled');
}).fail(function(req){
$(this).removeAttr('disabled');
$('#error-edit-title').text($.parseJSON(req.responseText).title);
});
return false;
});
$('#cancel').click(function(){
$('.edit-title').hide();
$('.show-title').show();
return false;
});
});
</script>

View File

@@ -1,145 +0,0 @@
@(issue: model.Issue,
comments: List[model.IssueComment],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div class="issue-avatar-image">@avatar(issue.openedUserName, 48)</div>
<div class="box issue-box">
<div class="box-content" style="padding: 0px;">
<div class="issue-header">
@if(hasWritePermission || loginAccount.map(_.userName == issue.openedUserName).getOrElse(false)){
<span class="pull-right"><a class="btn btn-small" href="#" id="edit">Edit</a></span>
}
<div class="small muted">
@user(issue.openedUserName, styleClass="username strong") opened this issue @datetime(issue.registeredDate)
</div>
<h4 id="issueTitle">@issue.title</h4>
</div>
<div class="issue-info">
<span id="label-assigned">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20) @user(userName, styleClass="username strong") is assigned
}.getOrElse("No one is assigned")
</span>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
}
<div class="pull-right">
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
Milestone: <span class="strong">@milestone.title</span>
}
}.getOrElse("No milestone")
</span>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount, false)
}
}
</div>
@if(hasWritePermission){
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
}
</div>
</div>
<div class="issue-content" id="issueContent">
@markdown(issue.content getOrElse "No description given.", repository, false, true)
</div>
</div>
</div>
<div class="issue-participants">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<span class="strong">@participants.size</span> @plural(participants.size, "participant")
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
}
</div>
<script>
$(function(){
$('#edit').click(function(){
$.get('@url(repository)/issues/_data/@issue.issueId',
{
dataType : 'html'
},
function(data){
$('#issueContent').empty().html(data);
});
return false;
});
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
},
function(){
$('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){
$('#label-assigned').text('No one is assigned');
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar').clone(false)).append(' ')
.append($('<a class="username strong">').attr('href', '@path/' + userName).text(userName))
.append(' is assigned');
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
}
});
});
$('a.milestone').click(function(){
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').text('No milestone');
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span>').append('Milestone: ').append($('<span class="strong">').text(title)));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
});
});
});
</script>

View File

@@ -0,0 +1,169 @@
@(issue: model.Issue,
comments: List[model.IssueComment],
issueLabels: List[model.Label],
collaborators: List[String],
milestones: List[(model.Milestone, Int, Int)],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import view.helpers._
<div style="margin-bottom: 8px;">
<span class="muted small strong">Labels</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
</div>
}
</div>
<ul class="label-list nav nav-pills nav-stacked">
@labellist(issueLabels)
</ul>
<hr/>
<div style="margin-bottom: 8px;">
<span class="muted small strong">Milestone</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="milestone" data-id=""><i class="icon-remove-circle"></i> Clear this milestone</a></li>
@milestones.filter(_._1.closedDate.isEmpty).map { case (milestone, _, _) =>
<li>
<a href="javascript:void(0);" class="milestone" data-id="@milestone.milestoneId" data-title="@milestone.title">
@helper.html.checkicon(Some(milestone.milestoneId) == issue.milestoneId) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="milestone-alert">Due by @date(dueDate)</span>
} else {
<span class="muted">Due by @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
</div>
}
</div>
<div id="milestone-progress-area">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, openCount, closeCount) if(milestone.milestoneId == milestoneId) =>
@issues.milestones.html.progress(openCount + closeCount, closeCount)
}
}
</div>
<span id="label-milestone">
@issue.milestoneId.map { milestoneId =>
@milestones.collect { case (milestone, _, _) if(milestone.milestoneId == milestoneId) =>
<span class="strong small">@milestone.title</span>
}
}.getOrElse(<span class="muted small">No milestone</span>)
</span>
<hr/>
<div style="margin-bottom: 8px;">
<span class="muted small strong">Assignee</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown() {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@helper.html.checkicon(Some(collaborator) == issue.assignedUserName)@avatar(collaborator, 20) @collaborator
</a>
</li>
}
}
</div>
}
</div>
<span id="label-assigned">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20) @user(userName, styleClass="username strong small")
}.getOrElse(<span class="muted small">No one</span>)
</span>
<hr/>
<div style="margin-bottom: 8px;">
@defining((issue.openedUserName :: comments.map(_.commentedUserName)).distinct){ participants =>
<div class="muted small strong">@participants.size @plural(participants.size, "participant")</div>
@participants.map { participant => @avatarLink(participant, 20, tooltip = true) }
}
</div>
<script>
$(function(){
$('a.toggle-label').click(function(){
var path, icon;
var i = $(this).children('i');
if(i.hasClass('icon-ok')){
path = 'delete';
icon = 'icon-white';
} else {
path = 'new';
icon = 'icon-ok';
}
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
{
labelId : $(this).data('label-id')
},
function(data){
i.removeClass().addClass(icon);
$('ul.label-list').empty().html(data);
});
return false;
});
$('a.milestone').click(function(){
var title = $(this).data('title');
var milestoneId = $(this).data('id');
$.post('@url(repository)/issues/@issue.issueId/milestone',
{
milestoneId: milestoneId
},
function(data){
console.log(data);
$('a.milestone i.icon-ok').attr('class', 'icon-white');
if(milestoneId == ''){
$('#label-milestone').html($('<span class="muted small">').text('No milestone'));
$('#milestone-progress-area').empty();
} else {
$('#label-milestone').html($('<span class="strong small">').text(title));
$('#milestone-progress-area').html(data);
$('a.milestone[data-id=' + milestoneId + '] i').attr('class', 'icon-ok');
}
});
});
$('a.assign').click(function(){
var $this = $(this);
var userName = $this.data('name');
$.post('@url(repository)/issues/@issue.issueId/assign',
{
assignedUserName: userName
},
function(){
$('a.assign i.icon-ok').attr('class', 'icon-white');
if(userName == ''){
$('#label-assigned').html($('<span class="muted small">').text('No one'));
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar-mini').clone(false)).append(' ')
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
$('a.assign[data-name=' + jqSelectorEscape(userName) + '] i').attr('class', 'icon-ok');
}
});
});
});
</script>

View File

@@ -1,4 +1,7 @@
@(issueLabels: List[model.Label]) @(issueLabels: List[model.Label])
@if(issueLabels.isEmpty){
<li><span class="muted small">None yet</span></li>
}
@issueLabels.map { label => @issueLabels.map { label =>
<li><span class="issue-label" style="background-color: #@label.color; color: #@label.fontColor;">@label.labelName</span></li> <li><span class="issue-label" style="background-color: #@label.color; color: #@label.fontColor;">@label.labelName</span></li>
} }

View File

@@ -1,51 +0,0 @@
@(issue: model.Issue,
issueLabels: List[model.Label],
labels: List[model.Label],
hasWritePermission: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import view.helpers._
<div style="margin-bottom: 8px;">
<span class="strong">Labels</span>
@if(hasWritePermission){
<div class="pull-right">
@helper.html.dropdown(right = true) {
@labels.map { label =>
<li>
<a href="#" class="toggle-label" data-label-id="@label.labelId">
@helper.html.checkicon(issueLabels.exists(_.labelId == label.labelId))
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
</div>
}
</div>
<ul class="label-list nav nav-pills nav-stacked">
@labellist(issueLabels)
</ul>
<script>
$(function(){
$('a.toggle-label').click(function(){
var path, icon;
var i = $(this).children('i');
if(i.hasClass('icon-ok')){
path = 'delete';
icon = 'icon-white';
} else {
path = 'new';
icon = 'icon-ok';
}
$.post('@url(repository)/issues/@issue.issueId/label/' + path,
{
labelId : $(this).data('label-id')
},
function(data){
i.removeClass().addClass(icon);
$('ul.label-list').empty().html(data);
});
return false;
});
});
</script>

View File

@@ -1,45 +1,61 @@
@(label: Option[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @(label: Option[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@defining((if(label.isEmpty) ("new", 190, 4) else ("edit", 180, 8))){ case (mode, width, margin) => @defining(label.map(_.labelId).getOrElse("new")){ labelId =>
<div id="@(mode)LabelArea"> <div id="edit-label-area-@labelId">
<form method="POST" id="edit-label-form" validate="true" style="margin-bottom: 8px;" <form style="margin-bottom: 0px;">
action="@url(repository)/issues/label/@{if(mode == "new") "new" else label.get.labelId + "/edit"}"> <input type="text" id="labelName-@labelId" style="width: 300px; margin-bottom: 0px;" value="@label.map(_.labelName)"@if(labelId == "new"){ placeholder="New label name"}/>
<span id="error-@(mode)LabelName" class="error"></span> <div id="label-color-@labelId" class="input-append color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; margin-bottom: 0px;">
<input type="text" name="@(mode)LabelName" id="@(mode)LabelName" style="width: @(width)px; margin-left: @(margin)px; margin-bottom: 0px;" value="@label.map(_.labelName)"@if(mode == "new"){ placeholder="New label name"}/> <input type="text" class="span3" id="labelColor-@labelId" value="#@label.map(_.color)" readonly style="width: 100px;">
<span id="error-@(mode)Color" class="error"></span>
<div class="input-append color bscp" data-color="#@label.map(_.color).getOrElse("888888")" data-color-format="hex" id="@(mode)Color" style="width: @(width)px; margin-bottom: 0px;">
<input type="text" class="span3" name="@(mode)Color" value="#@label.map(_.color)" readonly style="width: @(width - 12)px; margin-left: @(margin)px;">
<span class="add-on"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span> <span class="add-on"><i style="background-color: #@label.map(_.color).getOrElse("888888");"></i></span>
</div> </div>
<input type="submit" class="btn" style="margin-left: @(margin)px; margin-bottom: 0px;" value="@if(mode == "new"){Create} else {Save}"/> <script>
@if(mode == "edit"){ $('div#label-color-@labelId').colorpicker();
<input type="hidden" name="editLabelId" value="@label.map(_.labelId)"/> </script>
} <span id="label-error-@labelId" class="error" style="padding-left: 40px;"></span>
<span class="pull-right">
<input type="button" id="cancel-@labelId" class="btn label-edit-cancel" value="Cancel">
<input type="button" id="submit-@labelId" class="btn btn-success" style="margin-bottom: 0px;" value="@(if(labelId == "new") "Create label" else "Save changes")"/>
</span>
</form> </form>
<script> </div>
$(function(){ <script>
@if(mode == "new"){ $(function(){
$('#newColor').colorpicker(); $('#submit-@labelId').click(function(e){
$.post('@url(repository)/issues/labels/@{if(labelId == "new") "new" else labelId + "/edit"}', {
'labelName' : $('#labelName-@labelId').val(),
'labelColor': $('#labelColor-@labelId').val()
}, function(data, status){
$('div#edit-label-area-@labelId').remove();
@if(labelId == "new"){
$('#new-label-table').hide();
// Insert row into the top of table
$('#label-row-header').after(data);
} else {
// Replace table row
$('#label-row-@labelId').after(data).remove();
}
}).fail(function(xhr, status, error){
var errors = JSON.parse(xhr.responseText);
if(errors.labelName){
$('span#label-error-@labelId').text(errors.labelName);
} else if(errors.labelColor){
$('span#label-error-@labelId').text(errors.labelColor);
} else {
$('span#label-error-@labelId').text('error');
}
});
return false;
});
$('#cancel-@labelId').click(function(e){
$('div#edit-label-area-@labelId').remove();
@if(labelId == "new"){
$('#new-label-table').hide();
} else { } else {
$('#editColor').colorpicker(); $('#label-@labelId').show();
$('#edit-label-form').submit(function(e){
$.ajax($(this).attr('action'), {
type: 'POST',
data: $(this).serialize()
})
.done(function(data){
$('#label-edit').parent().empty().html(data);
})
.fail(function(data, status){
displayErrors($.parseJSON(data.responseText));
});
return false;
});
} }
}); });
</script> });
</div> </script>
} }

View File

@@ -1,47 +0,0 @@
@(labels: List[model.Label], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._
@import view.helpers._
<div id="label-edit">
<ul class="label-list nav nav-pills nav-stacked">
@labels.map { label =>
<li style="border: 1px solid white;">
<a href="javascript:void(0);" class="label-edit-link" data-label-id="@label.labelId">
<span class="count-right"><i class="icon-remove-circle"></i></span>
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName
</a>
</li>
}
</ul>
<script>
$(function(){
$('i.icon-remove-circle').click(function(e){
e.stopPropagation();
if(confirm('Are you sure you want to delete this?')){
$.get('@url(repository)/issues/label/' + $(this).parents('a').data('label-id') + '/delete',
function(data){
$('#label-edit').parent().empty().html(data);
}
);
}
});
$('a.label-edit-link').click(function(e){
if($('input[name=editLabelId]').val() != $(this).data('label-id')){
$('#editLabelArea').remove();
var element = this;
$.get('@url(repository)/issues/label/' + $(this).data('label-id') + '/edit',
function(data){
$(element).parent().append(data);
$('div#label-edit li').css('border', '1px solid white');
$(element).parent().css('border', '1px solid #eee');
}
);
} else {
$('#editLabelArea').remove();
$('div#label-edit li').css('border', '1px solid white');
}
});
});
</script>
</div>

View File

@@ -0,0 +1,36 @@
@(label: model.Label,
counts: Map[String, Int],
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
<tr id="label-row-@label.labelId">
<td style="padding-top: 15px; padding-bottom: 15px;">
<div class="milestone row-fluid" id="label-@label.labelId">
<div class="span8">
<div style="margin-top: 6px">
<a href="@url(repository)/issues?labels=@urlEncode(label.labelName)" id="label-row-content-@label.labelId">
<span style="background-color: #@label.color; color: #@label.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
<img src="@assets/common/images/label_@(if(label.fontColor == "ffffff") "white" else "black").png" style="width: 12px;"/>
@label.labelName
</span>
</a>
</div>
</div>
<div class="@if(hasWritePermission){span2} else {span4}">
<div class="pull-right">
<span class="muted">@counts.get(label.labelName).getOrElse(0) open issues</span>
</div>
</div>
@if(hasWritePermission){
<div class="span2">
<div class="pull-right">
<a href="javascript:void(0);" onclick="editLabel(@label.labelId)">Edit</a>
&nbsp;&nbsp;
<a href="javascript:void(0);" onclick="deleteLabel(@label.labelId)">Delete</a>
</div>
</div>
}
</div>
</td>
</tr>

View File

@@ -0,0 +1,66 @@
@(labels: List[model.Label],
counts: Map[String, Int],
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"Labels - ${repository.owner}/${repository.name}"){
@html.menu("issues", repository){
@issues.html.tab("labels", hasWritePermission, repository)
<table class="table table-bordered table-hover table-issues" id="new-label-table" style="display: none;">
<tr><td></td></tr>
</table>
<table class="table table-bordered table-hover table-issues">
<tr id="label-row-header">
<th style="background-color: #eee;">
<span class="small">@labels.size labels</span>
</th>
</tr>
@labels.map { label =>
@_root_.issues.labels.html.label(label, counts, repository, hasWritePermission)
}
@if(labels.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No labels to show.
@if(hasWritePermission){
<a href="@url(repository)/issues/labels/new">Create a new label.</a>
}
</td>
</tr>
}
</table>
}
}
<script>
$(function(){
$('#new-label-button').click(function(e){
if($('#new-label-area').size() != 0){
$('#new-label-table').hide();
$('#new-label-area').remove();
} else {
$.get('@url(repository)/issues/labels/new',
function(data){
$('#new-label-table').show().find('tr td').append(data);
}
);
}
});
});
function deleteLabel(labelId){
if(confirm('Once you delete this label, there is no going back.\nAre you sure?')){
$.post('@url(repository)/issues/labels/' + labelId + '/delete', function(){
$('tr#label-row-' + labelId).remove();
});
}
}
function editLabel(labelId){
$.get('@url(repository)/issues/labels/' + labelId + '/edit',
function(data){
$('#label-' + labelId).hide().parent().append(data);
}
);
}
</script>

View File

@@ -1,143 +1,25 @@
@(issues: List[(model.Issue, List[model.Label], Int)], @(target: String,
issues: List[service.IssuesService.IssueInfo],
page: Int, page: Int,
collaborators: List[String], collaborators: List[String],
milestones: List[model.Milestone], milestones: List[model.Milestone],
labels: List[model.Label], labels: List[model.Label],
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
allCount: Int,
assignedCount: Option[Int],
createdByCount: Option[Int],
labelCounts: Map[String, Int],
condition: service.IssuesService.IssueSearchCondition, condition: service.IssuesService.IssueSearchCondition,
filter: String,
repository: service.RepositoryService.RepositoryInfo, repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context) hasWritePermission: Boolean)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"Issues - ${repository.owner}/${repository.name}", Some(repository)){ @html.main((if(target == "issues") "Issues" else "Pull requests") + s" - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("issues", repository){ @html.menu(target, repository){
@tab("issues", false, repository) @tab(target, true, repository)
<div class="row-fluid"> @listparts(issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter == "all"){ class="active"}>
<a href="@url(repository)/issues@condition.toURL">
<span class="count-right">@allCount</span>
Everyone's Issues
</a>
</li>
@if(loginAccount.isDefined){
<li@if(filter == "assigned"){ class="active"}>
<a href="@url(repository)/issues/assigned/@loginAccount.map(_.userName)@condition.toURL">
<span class="count-right">@assignedCount</span>
Assigned to you
</a>
</li>
<li@if(filter == "created_by"){ class="active"}>
<a href="@url(repository)/issues/created_by/@loginAccount.map(_.userName)@condition.toURL">
<span class="count-right">@createdByCount</span>
Created by you
</a>
</li>
}
</ul>
<hr/>
@if(condition.milestoneId.isEmpty){
<span class="muted small">No milestone selected</span>
} else {
@if(condition.milestoneId.get.isEmpty){
<span class="muted small">Issues with no milestone</span>
} else {
<span class="muted small">Milestone:</span> @milestones.find(_.milestoneId == condition.milestoneId.get.get).map(_.title)
}
}
@helper.html.dropdown() {
@if(condition.milestoneId.isDefined){
<li>
<a href="@condition.copy(milestoneId = None).toURL">
<i class="icon-remove-circle"></i> Clear milestone filter
</a>
</li>
}
<li>
<a href="@condition.copy(milestoneId = Some(None)).toURL">
@helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
</a>
</li>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li>
<a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
<div class="small" style="padding-left: 20px;">
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
@if(condition.milestoneId.isDefined && condition.milestoneId.get.isDefined){
@milestones.find(_.milestoneId == condition.milestoneId.get.get).map { milestone =>
<div style="margin-top: 4px;">
@_root_.issues.milestones.html.progress(openCount + closedCount, closedCount, false)
</div>
<span class="muted small">@openCount open issues</span>
@if(milestone.closedDate.isDefined){
@milestone.closedDate.map { closedDate =>
<span class="small">Closed in @date(closedDate)</span>
}
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="small milestone-alert">Due in @date(dueDate)</span>
} else {
<span class="small">Due in @date(dueDate)</span>
}
}
}
}
}
<hr/>
<span class="strong">Labels</span>
<div>
<div id="label-list">
<ul class="label-list nav nav-pills nav-stacked">
@labels.map { label =>
<li>
<a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL"
@if(condition.labels.contains(label.labelName)){style="background-color: #@label.color; color: #@label.fontColor;"}>
<span class="count-right">@labelCounts.getOrElse(label.labelName, 0)</span>
<span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
@label.labelName
</a>
</li>
}
</ul>
</div>
</div>
@if(hasWritePermission){
<hr/>
<input type="button" class="btn btn-block" id="manageLabel" data-toggle="button" value="Manage Labels"/>
<br/>
<span class="strong">New label</span>
@_root_.issues.labels.html.edit(None, repository)
}
</div>
@***** show issue list *****@
@listparts(issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), hasWritePermission)
</div>
@if(hasWritePermission){ @if(hasWritePermission){
<form id="batcheditForm" method="POST"> <form id="batcheditForm" method="POST">
<input type="hidden" name="value"/> <input type="hidden" name="value"/>
<input type="hidden" name="checked"/> <input type="hidden" name="checked"/>
<input type="hidden" name="from" value="@target"/>
</form> </form>
} }
} }
@@ -145,20 +27,34 @@
@if(hasWritePermission){ @if(hasWritePermission){
<script> <script>
$(function(){ $(function(){
$('#manageLabel').click(function(){ $('a.header-link').mouseover(function(e){
if($(this).data('toggle-state')){ var target = e.target;
location.href = '@url(repository)/issues'; if(e.target.tagName != 'A'){
} else { target = e.target.parentElement;
$(this).data('toggle-state', 'on');
$.get('@url(repository)/issues/label/edit', function(data){
$('#label-list').parent().empty().html(data);
});
} }
$(target).children('strong' ).css('color', '#0088cc');
$(target).children('img.header-icon-hover').css('display', 'inline');
$(target).children('img.header-icon' ).css('display', 'none');
});
$('a.header-link').mouseout(function(e){
var target = e.target;
if(e.target.tagName != 'A'){
target = e.target.parentElement;
}
$(target).children('strong' ).css('color', 'black');
$(target).children('img.header-icon-hover').css('display', 'none');
$(target).children('img.header-icon' ).css('display', 'inline');
}); });
$('.table-issues input[type=checkbox]').change(function(){ $('.table-issues input[type=checkbox]').change(function(){
$('.table-issues button').prop('disabled', if($('.table-issues input[type=checkbox]').filter(':checked').length == 0){
!$('.table-issues input[type=checkbox]').filter(':checked').length); $('#table-issues-control').show();
$('#table-issues-batchedit').hide();
} else {
$('#table-issues-control').hide();
$('#table-issues-batchedit').show();
}
}).filter(':first').change(); }).filter(':first').change();
var submitBatchEdit = function(action, value) { var submitBatchEdit = function(action, value) {
@@ -170,8 +66,8 @@ $(function(){
form.submit(); form.submit();
}; };
$('#state').click(function(){ $('a.toggle-state').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).text().toLowerCase()); submitBatchEdit('@url(repository)/issues/batchedit/state', $(this).data('id'));
}); });
$('a.toggle-label').click(function(){ $('a.toggle-label').click(function(){
submitBatchEdit('@url(repository)/issues/batchedit/label', $(this).data('id')); submitBatchEdit('@url(repository)/issues/batchedit/label', $(this).data('id'));

View File

@@ -1,4 +1,4 @@
@(issues: List[(model.Issue, List[model.Label], Int)], @(issues: List[service.IssuesService.IssueInfo],
page: Int, page: Int,
openCount: Int, openCount: Int,
closedCount: Int, closedCount: Int,
@@ -10,175 +10,195 @@
hasWritePermission: Boolean = false)(implicit context: app.Context) hasWritePermission: Boolean = false)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@import service.IssuesService.IssueInfo
<div class="span9"> @if(condition.nonEmpty){
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){ <div>
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL" id="clear-filter"> <a href="@service.IssuesService.IssueSearchCondition().toURL" class="header-link">
<i class="icon-remove-circle"></i> Clear milestone and label filters <img src="@assets/common/images/clear.png" class="header-icon"/>
<img src="@assets/common/images/clear_hover.png" class="header-icon-hover" style="display: none;"/>
<span class="strong">Clear current search query, filters, and sorts</span>
</a>
</div>
}
<table class="table table-bordered table-hover table-issues">
<tr>
<th style="background-color: #eee;">
<input type="checkbox"/>
<span class="small">
<a class="button-link@if(condition.state == "open"){ selected}" href="@condition.copy(state = "open").toURL">
<img src="@assets/common/images/status-open@(if(condition.state == "open"){"-active"}).png"/>
@openCount Open
</a>&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> </a>
} </span>
@if(condition.repo.isDefined){ <div class="pull-right" id="table-issues-control">
<a href="@condition.copy(repo = None).toURL" id="clear-filter"> @helper.html.dropdown("Author", flat = true) {
<i class="icon-remove-circle"></i> Clear filter on @condition.repo @collaborators.map { collaborator =>
</a> <li>
} <a href="@condition.copy(author = (if(condition.author == Some(collaborator)) None else Some(collaborator))).toURL">
<div class="pull-right"> @helper.html.checkicon(condition.author == Some(collaborator))
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 7, condition.toURL) @avatar(collaborator, 20) @collaborator
</div> </a>
<div class="btn-group"> </li>
<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("Label", flat = true) {
@helper.html.dropdown( @labels.map { label =>
value = (condition.sort, condition.direction) match { <li>
case ("created" , "desc") => "Newest" <a href="@condition.copy(labels = (if(condition.labels.contains(label.labelName)) condition.labels - label.labelName else condition.labels + label.labelName)).toURL">
case ("created" , "asc" ) => "Oldest" @helper.html.checkicon(condition.labels.contains(label.labelName))
case ("comments", "desc") => "Most commented" <span style="background-color: #@label.color;" class="label-color">&nbsp;&nbsp;</span>
case ("comments", "asc" ) => "Least commented" @label.labelName
case ("updated" , "desc") => "Recently updated" </a>
case ("updated" , "asc" ) => "Least recently updated" </li>
}, }
prefix = "Sort", }
mini = false @helper.html.dropdown("Milestone", flat = true) {
){ <li>
<li> <a href="@condition.copy(milestoneId = Some(None)).toURL">
<a href="@condition.copy(sort="created", direction="desc").toURL"> @helper.html.checkicon(condition.milestoneId == Some(None)) Issues with no milestone
@helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest </a>
</a> </li>
</li> @milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li> <li>
<a href="@condition.copy(sort="created", direction="asc" ).toURL"> <a href="@condition.copy(milestoneId = Some(Some(milestone.milestoneId))).toURL">
@helper.html.checkicon(condition.sort == "created" && condition.direction == "asc") Oldest @helper.html.checkicon(condition.milestoneId == Some(Some(milestone.milestoneId))) @milestone.title
</a> </a>
</li> </li>
<li> }
<a href="@condition.copy(sort="comments", direction="desc").toURL"> }
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented @helper.html.dropdown("Assignee", flat = true) {
</a> @collaborators.map { collaborator =>
</li> <li>
<li> <a href="@condition.copy(assigned = Some(collaborator)).toURL">
<a href="@condition.copy(sort="comments", direction="asc" ).toURL"> @helper.html.checkicon(condition.assigned == Some(collaborator))
@helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented @avatar(collaborator, 20) @collaborator
</a> </a>
</li> </li>
<li> }
<a href="@condition.copy(sort="updated", direction="desc").toURL"> }
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated @helper.html.dropdown("Sort", flat = true){
</a> <li>
</li> <a href="@condition.copy(sort="created", direction="desc").toURL">
<li> @helper.html.checkicon(condition.sort == "created" && condition.direction == "desc") Newest
<a href="@condition.copy(sort="updated", direction="asc" ).toURL"> </a>
@helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated </li>
</a> <li>
</li> <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>
@if(issues.isEmpty){ </li>
<tr> <li>
<td style="padding: 20px; background-color: #eee; text-align: center;"> <a href="@condition.copy(sort="comments", direction="desc").toURL">
No issues to show. @helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){ </a>
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a> </li>
} else { <li>
@if(repository.isDefined){ <a href="@condition.copy(sort="comments", direction="asc" ).toURL">
<a href="@url(repository.get)/issues/new">Create a new issue.</a> @helper.html.checkicon(condition.sort == "comments" && condition.direction == "asc") Least commented
} </a>
} </li>
</td> <li>
</tr> <a href="@condition.copy(sort="updated", direction="desc").toURL">
} else { @helper.html.checkicon(condition.sort == "updated" && condition.direction == "desc") Recently updated
@if(hasWritePermission){ </a>
<tr> </li>
<td style="background-color: #eee;"> <li>
<div class="btn-group"> <a href="@condition.copy(sort="updated", direction="asc" ).toURL">
<button class="btn btn-mini strong" id="state">@{if(condition.state == "open") "Close" else "Reopen"}</button> @helper.html.checkicon(condition.sort == "updated" && condition.direction == "asc") Least recently updated
</div> </a>
@helper.html.dropdown("Label") { </li>
@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_mono.png"/>Due in @date(dueDate)
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
}
</div>
</a>
</li>
}
}
</td>
</tr>
} }
}
@issues.map { case (issue, labels, commentCount) =>
<tr>
<td>
@if(hasWritePermission){
<label class="checkbox" style="cursor: default;">
<input type="checkbox" value="@issue.issueId"/>
}
@if(issue.isPullRequest){
<img src="@assets/common/images/pullreq-@(if(issue.closed) "closed" else "open").png"/>
} else {
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png"/>
}
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
@if(issue.isPullRequest){
<a href="@path/@issue.userName/@issue.repositoryName/pull/@issue.issueId" class="issue-title">@issue.title</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
}
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
#@issue.issueId
</span>
<div class="small muted" style="margin-left: 20px;">
Opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)&nbsp;
@if(commentCount > 0){
<i class="icon-comment"></i><a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">@commentCount @plural(commentCount, "comment")</a>
}
</div>
@if(hasWritePermission){
</label>
}
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div> </div>
</div> @if(hasWritePermission){
<div class="pull-right" id="table-issues-batchedit">
@helper.html.dropdown("Mark as", flat = true) {
<li><a href="javascript:void(0);" class="toggle-state" data-id="open">Open</a></li>
<li><a href="javascript:void(0);" class="toggle-state" data-id="close">Close</a></li>
}
@helper.html.dropdown("Label", flat = true) {
@labels.map { label =>
<li>
<a href="javascript:void(0);" class="toggle-label" data-id="@label.labelId">
<i class="icon-white"></i>
<span class="label" style="background-color: #@label.color;">&nbsp;</span>
@label.labelName
</a>
</li>
}
}
@helper.html.dropdown("Milestone", flat = true) {
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="">No milestone</a></li>
@milestones.filter(_.closedDate.isEmpty).map { milestone =>
<li><a href="javascript:void(0);" class="toggle-milestone" data-id="@milestone.milestoneId">@milestone.title</a></li>
}
}
@helper.html.dropdown("Assignee", flat = true) {
<li><a href="javascript:void(0);" class="toggle-assign" data-name=""><i class="icon-remove-circle"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li><a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator"><i class="icon-white"></i>@avatar(collaborator, 20) @collaborator</a></li>
}
}
</div>
}
</th>
</tr>
@if(issues.isEmpty){
<tr>
<td style="padding: 20px; background-color: #eee; text-align: center;">
No issues to show.
@if(condition.labels.nonEmpty || condition.milestoneId.isDefined){
<a href="@condition.copy(labels = Set.empty, milestoneId = None).toURL">Clear active filters.</a>
} else {
@if(repository.isDefined){
<a href="@url(repository.get)/issues/new">Create a new issue.</a>
}
}
</td>
</tr>
}
@issues.map { case IssueInfo(issue, labels, milestone, commentCount) =>
<tr>
<td style="padding-top: 15px; padding-bottom: 15px;">
@if(hasWritePermission){
<input type="checkbox" value="@issue.issueId"/>
}
<img src="@assets/common/images/issue-@(if(issue.closed) "closed" else "open").png" style="margin-right: 20px;"/>
@if(repository.isEmpty){
<a href="@path/@issue.userName/@issue.repositoryName">@issue.repositoryName</a>&nbsp;&#xFF65;
}
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-title">@issue.title</a>
@labels.map { label =>
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right small">
@issue.assignedUserName.map { userName =>
@avatar(userName, 20, tooltip = true)
}
@if(commentCount > 0){
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<img src="@assets/common/images/comment-active.png"> @commentCount
</a>
} else {
<a href="@path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count" style="color: silver;">
<img src="@assets/common/images/comment.png"> @commentCount
</a>
}
</span>
<div class="small muted" style="margin-left: 40px; margin-top: 5px;">
#@issue.issueId opened by @user(issue.openedUserName, styleClass="username") @datetime(issue.registeredDate)
@milestone.map { milestone =>
<span style="margin: 20px;"><a href="@condition.copy(milestoneId = Some(Some(1))).toURL" class="username"><img src="@assets/common/images/milestone.png"> @milestone</a></span>
}
</div>
</td>
</tr>
}
</table>
<div class="pull-right">
@helper.html.paginator(page, (if(condition.state == "open") openCount else closedCount), service.IssuesService.IssueLimit, 10, condition.toURL)
</div>

View File

@@ -2,12 +2,18 @@
@import context._ @import context._
@import view.helpers._ @import view.helpers._
@html.main(s"Milestones - ${repository.owner}/${repository.name}"){ @html.main(s"Milestones - ${repository.owner}/${repository.name}"){
@html.menu("milestones", repository){ @html.menu("issues", repository){
@issues.html.tab("milestones", false, repository) @if(milestone.isEmpty){
<h4>New milestone</h4>
<div class="muted">Create a new milestone to help organize your issues and pull requests.</div>
} else {
@issues.html.tab("milestones", false, repository)
<br><br>
}
<hr style="margin-top: 12px; margin-bottom: 18px;" class="fill-width"/>
<form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true"> <form method="POST" action="@url(repository)/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
<fieldset> <fieldset>
<label for="title"><string>Title</string></label> <input type="text" id="title" name="title" style="width: 500px;" value="@milestone.map(_.title)" placeholder="Title"/>
<input type="text" id="title" name="title" style="width: 400px;" value="@milestone.map(_.title)"/>
<span id="error-title" class="error"></span> <span id="error-title" class="error"></span>
</fieldset> </fieldset>
<fieldset> <fieldset>

View File

@@ -6,94 +6,93 @@
@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", false, repository) @issues.html.tab("milestones", hasWritePermission, repository)
<div class="row-fluid"> <table class="table table-bordered table-hover table-issues">
<div class="span3"> <tr>
<ul class="nav nav-pills nav-stacked"> <th style="background-color: #eee;">
<li@if(state == "open"){ class="active"}> <span class="small">
<a href="?state=open"> <a class="button-link@if(state == "open"){ selected}" href="?state=open">
<span class="count-right">@milestones.filter(_._1.closedDate.isEmpty).size</span> <img src="@assets/common/images/milestone@(if(state == "open"){"-active"}).png"/>
Open Milestones @milestones.filter(_._1.closedDate.isEmpty).size Open
</a>&nbsp;&nbsp;
<a class="button-link@if(state == "closed"){ selected}" href="?state=closed">
<img src="@assets/common/images/milestone@(if(state == "closed"){"-active"}).png"/>
@milestones.filter(_._1.closedDate.isDefined).size Closed
</a> </a>
</li> </span>
<li@if(state == "closed"){ class="active"}> </th>
<a href="?state=closed"> </tr>
<span class="count-right">@milestones.filter(_._1.closedDate.isDefined).size</span> @defining(milestones.filter { case (milestone, _, _) =>
Closed Milestones milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open")
</a> }){ milestones =>
</li> @milestones.map { case (milestone, openCount, closedCount) =>
</ul> <tr>
@if(hasWritePermission){ <td style="padding-top: 15px; padding-bottom: 15px;">
<hr> <div class="milestone row-fluid">
<a href="@url(repository)/issues/milestones/new" class="btn btn-block">Create a new milestone</a> <div class="span4">
} <a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a>
</div> <div style="margin-top: 6px">
<div class="span9"> @if(milestone.closedDate.isDefined){
<table class="table table-bordered table-hover"> <span class="muted">Closed @datetime(milestone.closedDate.get)</span>
@defining(milestones.filter { case (milestone, _, _) => } else {
milestone.closedDate.map(_ => state == "closed").getOrElse(state == "open") @milestone.dueDate.map { dueDate =>
}){ milestones => @if(isPast(dueDate)){
@milestones.map { case (milestone, openCount, closedCount) => <img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due by @date(dueDate)</span>
<tr> } else {
<td> <span class="muted">Due by @date(dueDate)</span>
<div class="milestone row-fluid">
<div class="span4">
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open" class="milestone-title">@milestone.title</a><br>
@if(milestone.closedDate.isDefined){
<span class="muted">Closed @datetime(milestone.closedDate.get)</span>
} else {
@milestone.dueDate.map { dueDate =>
@if(isPast(dueDate)){
<img src="@assets/common/images/alert.png"/><span class="muted milestone-alert">Due in @date(dueDate)</span>
} else {
<span class="muted">Due in @date(dueDate)</span>
}
}.getOrElse {
<span class="muted">No due date</span>
} }
}.getOrElse {
<span class="muted">No due date</span>
}
}
</div>
</div>
<div class="span8">
@progress(openCount + closedCount, closedCount)
<div>
<div>
@if(closedCount == 0){
0%
} else {
@((closedCount.toDouble / (openCount + closedCount).toDouble * 100).toInt)%
} <span class="muted">complete</span> &nbsp;&nbsp;
@openCount <span class="muted">open</span> &nbsp;&nbsp;
@closedCount <span class="muted">closed</span>
</div>
<div class="milestone-menu">
@if(hasWritePermission){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/edit">Edit</a> &nbsp;&nbsp;
@if(milestone.closedDate.isDefined){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/open">Open</a> &nbsp;&nbsp;
} else {
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/close">Close</a> &nbsp;&nbsp;
}
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/delete" class="delete">Delete</a>
} }
</div> </div>
<div class="span8">
<div class="milestone-menu">
<div class="pull-right">
@if(hasWritePermission){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/edit">Edit
@if(milestone.closedDate.isDefined){
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/open">Open</a>
} else {
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/close">Close</a>
}
<a href="@url(repository)/issues/milestones/@milestone.milestoneId/delete" class="delete">Delete</a>
}
<a href="@url(repository)/issues?milestone=@milestone.milestoneId&state=open">Browse issues</a>
</div>
<span class="muted">@closedCount closed - @openCount open</span>
</div>
@progress(openCount + closedCount, closedCount, true)
</div>
</div> </div>
@if(milestone.description.isDefined){ </div>
<div class="milestone-description"> </div>
@markdown(milestone.description.get, repository, false, false) @if(milestone.description.isDefined){
</div> <div class="milestone-description">
} @markdown(milestone.description.get, repository, false, false)
</td> </div>
</tr>
} }
@if(milestones.isEmpty){ </td>
<tr> </tr>
<td style="padding: 20px; background-color: #eee; text-align: center;"> }
No milestones to show. @if(milestones.isEmpty){
@if(hasWritePermission){ <tr>
<a href="@url(repository)/issues/milestones/new">Create a new milestone.</a> <td style="padding: 20px; background-color: #eee; text-align: center;">
} No milestones to show.
</td> @if(hasWritePermission){
</tr> <a href="@url(repository)/issues/milestones/new">Create a new milestone.</a>
} }
} </td>
</table> </tr>
</div> }
</div> }
</table>
} }
} }
<script> <script>

View File

@@ -1,15 +1,6 @@
@(total: Int, progress: Int, showPercentage: Boolean) @(total: Int, progress: Int)
<div class="milestone-progress"> <div class="milestone-progress">
@if(progress > 0){ @if(progress > 0){
<span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span> <span class="milestone-progress" style="width: @((progress.toDouble / total.toDouble * 100).toInt)%;"></span>
} }
@if(showPercentage){
<span class="milestone-percentage">
@if(progress == 0){
0%
} else {
@((progress.toDouble / total.toDouble * 100).toInt)%
}
</span>
}
</div> </div>

View File

@@ -1,16 +1,28 @@
@(active: String, create: Boolean, repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context) @(active: String, newButton: Boolean,
repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
@import context._ @import context._
@import view.helpers._ @import view.helpers._
<ul class="nav nav-tabs pull-left fill-width"> <ul class="nav nav-pills-group pull-left fill-width">
<li@if(active == "issues"){ class="active"}><a href="@url(repository)/issues">Browse Issues</a></li> <li class="@if(active == "issues" ){active} first"><a href="@url(repository)/issues">Issues</a></li>
<li@if(active == "milestones"){ class="active"}><a href="@url(repository)/issues/milestones">Milestones</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){ @if(loginAccount.isDefined){
<li class="pull-right"> <li class="pull-right">
<div class="btn-group"> <div class="btn-group">
@if(create){ @if(newButton){
<a class="btn btn-small btn-success" href="#" disabled="disabled">New Issue</a> @if(active == "issues"){
} else { <a class="btn btn-success" href="@url(repository)/issues/new">New issue</a>
<a class="btn btn-small 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> </div>
</li> </li>

View File

@@ -11,7 +11,6 @@
@import view.helpers._ @import view.helpers._
<div class="row-fluid"> <div class="row-fluid">
<div class="span10"> <div class="span10">
@issues.html.issuedetail(issue, comments, collaborators, milestones, hasWritePermission, repository)
@issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq)) @issues.html.commentlist(issue, comments, hasWritePermission, repository, Some(pullreq))
@defining(comments.exists(_.action == "merge")){ merged => @defining(comments.exists(_.action == "merge")){ merged =>
@if(hasWritePermission && !issue.closed){ @if(hasWritePermission && !issue.closed){
@@ -67,7 +66,7 @@
</div> </div>
} }
<hr/> <hr/>
@issues.html.labels(issue, issueLabels, labels, hasWritePermission, repository) @issues.html.issueinfo(issue, comments, issueLabels, collaborators, milestones, labels, hasWritePermission, repository)
</div> </div>
</div> </div>
<script> <script>

View File

@@ -1,51 +0,0 @@
@(issues: List[(model.Issue, List[model.Label], Int)],
counts: List[service.PullRequestService.PullRequestCount],
filter: Option[String],
page: Int,
openCount: Int,
closedCount: Int,
allCount: Int,
condition: service.IssuesService.IssueSearchCondition,
repository: service.RepositoryService.RepositoryInfo,
hasWritePermission: Boolean)(implicit context: app.Context)
@import context._
@import view.helpers._
@html.main(s"Pull Requests - ${repository.owner}/${repository.name}", Some(repository)){
@html.menu("pulls", repository){
<div class="row-fluid">
<div class="span3">
<ul class="nav nav-pills nav-stacked">
<li@if(filter.isEmpty){ class="active"}>
<a href="@url(repository)/pulls">
<span class="count-right">@allCount</span>
All Requests
</a>
</li>
@if(loginAccount.isDefined){
<li@if(filter.map(_ == loginAccount.get.userName).getOrElse(false)){ class="active"}>
<a href="@url(repository)/pulls/@loginAccount.map(_.userName)">
<span class="count-right">@counts.find(_.userName == loginAccount.get.userName).map(_.count).getOrElse(0)</span>
Yours
</a>
</li>
}
</ul>
<hr>
<ul class="nav nav-pills nav-stacked small">
@counts.map { user =>
@if(loginAccount.isEmpty || loginAccount.get.userName != user.userName){
<li@if(filter.map(_ == user.userName).getOrElse(false)){ class="active"}>
<a href="@url(repository)/pulls/@user.userName">
<span class="count-right">@user.count</span>
@user.userName
</a>
</li>
}
}
</ul>
</div>
@listparts(issues, page, openCount, closedCount, condition, Some(repository), hasWritePermission)
</div>
}
}

View File

@@ -617,11 +617,81 @@ span.simplified-path {
color: #888; color: #888;
} }
/****************************************************************************/
/* nav pulls group */
/****************************************************************************/
.nav-pills-group:after {
display: table;
line-height: 0;
content: "";
}
.nav-pills-group:after {
clear: both;
}
.nav-pills-group > li {
float: left;
}
.nav-pills-group > li > a {
padding-right: 12px;
padding-left: 12px;
line-height: 14px;
color: #666;
font-weight: bold;
}
.nav-pills-group > li > a {
padding-top: 10px;
padding-bottom: 10px;
border-left : 1px solid #e5e5e5;
border-top : 1px solid #e5e5e5;
border-bottom : 1px solid #e5e5e5;
}
.nav-pills-group > .first > a {
-webkit-border-radius: 4px 0 0 4px;
-moz-border-radius: 4px 0 0 4px;
border-radius: 4px 0 0 4px;
}
.nav-pills-group > .last > a {
-webkit-border-radius: 0 4px 4px 0;
-moz-border-radius: 0 4px 4px 0;
border-radius: 0 4px 4px 0;
border-right : 1px solid #e5e5e5;
}
.nav-pills-group > .active > a,
.nav-pills-group > .active > a:hover,
.nav-pills-group > .active > a:focus {
color: #ffffff;
background-color: #0088cc;
border-color: #0088cc;
}
/****************************************************************************/ /****************************************************************************/
/* Issues */ /* Issues */
/****************************************************************************/ /****************************************************************************/
.btn-group.open .dropdown-toggle.flat {
background-image: none;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}
a.button-link {
font-weight: normal;
color: gray;
}
a.selected {
font-weight: bold;
color: black;
}
span.issue-status { span.issue-status {
display: block;
font-size: large; font-size: large;
text-align: center; text-align: center;
padding: 8px; padding: 8px;
@@ -634,7 +704,7 @@ table.table-issues {
a.issue-title { a.issue-title {
color: #333; color: #333;
font-weight: bold; font-weight: bold;
size: 110%; font-size: 120%;
} }
ul.label-list { ul.label-list {
@@ -671,8 +741,7 @@ span.milestone-alert {
} }
a.milestone-title { a.milestone-title {
font-size: 120%; font-size: 180%;
font-weight: bold;
} }
div.milestone-description { div.milestone-description {
@@ -680,13 +749,12 @@ div.milestone-description {
color: #666; color: #666;
} }
div.milestone-menu { a.milestone-title {
font-size: 80%; color: #333;
} }
div.milestone-menu a { div.milestone-menu {
margin-left: 8px; margin-top: 8px;
font-weight: bold;
} }
div.milestone-menu a.delete { div.milestone-menu a.delete {
@@ -698,13 +766,13 @@ div#milestone-progress-area {
} }
div#milestone-progress-area div.milestone-progress { div#milestone-progress-area div.milestone-progress {
width: 150px; width: 130px;
margin-bottom: -6px; margin-bottom: -6px;
} }
div.milestone-progress { div.milestone-progress {
position: relative; position: relative;
height: 20px; height: 10px;
color: white; color: white;
margin-bottom: 4px; margin-bottom: 4px;
font-weight: bold; font-weight: bold;
@@ -725,11 +793,6 @@ span.milestone-progress {
-moz-border-radius: 4px; -moz-border-radius: 4px;
} }
span.milestone-percentage {
position: absolute;
padding-left: 8px;
}
div.issue-header { div.issue-header {
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B