mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-09 15:05:50 +01:00
Start to implement milestone pages.
This commit is contained in:
@@ -3,21 +3,31 @@ package app
|
|||||||
import jp.sf.amateras.scalatra.forms._
|
import jp.sf.amateras.scalatra.forms._
|
||||||
|
|
||||||
import service._
|
import service._
|
||||||
import util.UsersOnlyAuthenticator
|
import util.{WritableRepositoryAuthenticator, ReadableRepositoryAuthenticator, UsersOnlyAuthenticator}
|
||||||
|
|
||||||
class IssuesController extends IssuesControllerBase
|
class IssuesController extends IssuesControllerBase
|
||||||
with IssuesService with RepositoryService with AccountService with UsersOnlyAuthenticator
|
with IssuesService with RepositoryService with AccountService
|
||||||
|
with UsersOnlyAuthenticator with ReadableRepositoryAuthenticator with WritableRepositoryAuthenticator
|
||||||
|
|
||||||
trait IssuesControllerBase extends ControllerBase {
|
trait IssuesControllerBase extends ControllerBase {
|
||||||
self: IssuesService with RepositoryService with UsersOnlyAuthenticator =>
|
self: IssuesService with RepositoryService
|
||||||
|
with UsersOnlyAuthenticator with ReadableRepositoryAuthenticator with WritableRepositoryAuthenticator =>
|
||||||
|
|
||||||
case class IssueForm(title: String, content: Option[String])
|
case class IssueForm(title: String, content: Option[String])
|
||||||
|
|
||||||
|
case class MilestoneForm(title: String, description: Option[String], dueDate: Option[java.sql.Date])
|
||||||
|
|
||||||
val form = mapping(
|
val form = mapping(
|
||||||
"title" -> trim(label("Title", text(required))),
|
"title" -> trim(label("Title", text(required))),
|
||||||
"content" -> trim(optional(text()))
|
"content" -> trim(optional(text()))
|
||||||
)(IssueForm.apply)
|
)(IssueForm.apply)
|
||||||
|
|
||||||
|
val milestoneForm = mapping(
|
||||||
|
"title" -> trim(label("Title", text(required))),
|
||||||
|
"description" -> trim(label("Description", optional(text()))),
|
||||||
|
"dueDate" -> trim(label("Due Date", optional(date())))
|
||||||
|
)(MilestoneForm.apply)
|
||||||
|
|
||||||
get("/:owner/:repository/issues"){
|
get("/:owner/:repository/issues"){
|
||||||
val owner = params("owner")
|
val owner = params("owner")
|
||||||
val repository = params("repository")
|
val repository = params("repository")
|
||||||
@@ -53,4 +63,86 @@ trait IssuesControllerBase extends ControllerBase {
|
|||||||
saveIssue(owner, repository, context.loginAccount.get.userName, form.title, form.content)))
|
saveIssue(owner, repository, context.loginAccount.get.userName, form.title, form.content)))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/issues/milestones")(readableRepository {
|
||||||
|
val owner = params("owner")
|
||||||
|
val repository = params("repository")
|
||||||
|
val state = params.getOrElse("state", "open")
|
||||||
|
|
||||||
|
getRepository(owner, repository, baseUrl) match {
|
||||||
|
case None => NotFound()
|
||||||
|
case Some(r) => issues.html.milestones(state, getMilestones(owner, repository), r, isWritable(owner, repository, context.loginAccount))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/issues/milestones/new")(writableRepository {
|
||||||
|
val owner = params("owner")
|
||||||
|
val repository = params("repository")
|
||||||
|
|
||||||
|
getRepository(owner, repository, baseUrl) match {
|
||||||
|
case None => NotFound()
|
||||||
|
case Some(r) => issues.html.milestoneedit(None, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/issues/milestones/new", milestoneForm)(writableRepository { form =>
|
||||||
|
val owner = params("owner")
|
||||||
|
val repository = params("repository")
|
||||||
|
|
||||||
|
createMilestone(owner, repository, form.title, form.description, form.dueDate)
|
||||||
|
|
||||||
|
redirect("/%s/%s/issues/milestones".format(owner, repository))
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/issues/milestones/:milestoneId/edit")(writableRepository {
|
||||||
|
val owner = params("owner")
|
||||||
|
val repository = params("repository")
|
||||||
|
val milestoneId = params("milestoneId").toInt
|
||||||
|
|
||||||
|
getRepository(owner, repository, baseUrl) match {
|
||||||
|
case None => NotFound()
|
||||||
|
case Some(r) => issues.html.milestoneedit(getMilestone(owner, repository, milestoneId), r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
post("/:owner/:repository/issues/milestones/:milestoneId/edit", milestoneForm)(writableRepository { form =>
|
||||||
|
val owner = params("owner")
|
||||||
|
val repository = params("repository")
|
||||||
|
val milestoneId = params("milestoneId").toInt
|
||||||
|
|
||||||
|
getMilestone(owner, repository, milestoneId) match {
|
||||||
|
case None => NotFound()
|
||||||
|
case Some(m) => {
|
||||||
|
updateMilestone(m.copy(title = form.title, description = form.description, dueDate = form.dueDate))
|
||||||
|
redirect("/%s/%s/issues/milestones".format(owner, repository))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/issues/milestones/:milestoneId/close")(writableRepository {
|
||||||
|
val owner = params("owner")
|
||||||
|
val repository = params("repository")
|
||||||
|
val milestoneId = params("milestoneId").toInt
|
||||||
|
|
||||||
|
getMilestone(owner, repository, milestoneId) match {
|
||||||
|
case None => NotFound()
|
||||||
|
case Some(m) => {
|
||||||
|
updateMilestone(m.copy(closed = true))
|
||||||
|
redirect("/%s/%s/issues/milestones".format(owner, repository))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
get("/:owner/:repository/issues/milestones/:milestoneId/open")(writableRepository {
|
||||||
|
val owner = params("owner")
|
||||||
|
val repository = params("repository")
|
||||||
|
val milestoneId = params("milestoneId").toInt
|
||||||
|
|
||||||
|
getMilestone(owner, repository, milestoneId) match {
|
||||||
|
case None => NotFound()
|
||||||
|
case Some(m) => {
|
||||||
|
updateMilestone(m.copy(closed = false))
|
||||||
|
redirect("/%s/%s/issues/milestones".format(owner, repository))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -50,4 +50,31 @@ trait IssuesService {
|
|||||||
}.map(_.issueId).update(id) > 0
|
}.map(_.issueId).update(id) > 0
|
||||||
} get
|
} get
|
||||||
|
|
||||||
|
def createMilestone(owner: String, repository: String,
|
||||||
|
title: String, description: Option[String], dueDate: Option[java.sql.Date]): Unit = {
|
||||||
|
Milestones.ins insert (owner, repository, title, description, dueDate, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
def updateMilestone(milestone: Milestone): Unit =
|
||||||
|
Query(Milestones)
|
||||||
|
.filter { m => (m.userName is milestone.userName.bind) && (m.repositoryName is milestone.repositoryName.bind) && (m.milestoneId is milestone.milestoneId.bind)}
|
||||||
|
.map { m => m.title ~ m.description.? ~ m.dueDate.? ~ m.closed }
|
||||||
|
.update (
|
||||||
|
milestone.title,
|
||||||
|
milestone.description,
|
||||||
|
milestone.dueDate,
|
||||||
|
milestone.closed)
|
||||||
|
|
||||||
|
def getMilestone(owner: String, repository: String, milestoneId: Int): Option[Milestone] =
|
||||||
|
Query(Milestones)
|
||||||
|
.filter(m => (m.userName is owner.bind) && (m.repositoryName is repository.bind) && (m.milestoneId is milestoneId.bind))
|
||||||
|
.sortBy(_.milestoneId desc)
|
||||||
|
.firstOption
|
||||||
|
|
||||||
|
def getMilestones(owner: String, repository: String): List[Milestone] =
|
||||||
|
Query(Milestones)
|
||||||
|
.filter(m => (m.userName is owner.bind) && (m.repositoryName is repository.bind))
|
||||||
|
.sortBy(_.milestoneId desc)
|
||||||
|
.list
|
||||||
|
|
||||||
}
|
}
|
||||||
47
src/main/twirl/issues/milestoneedit.scala.html
Normal file
47
src/main/twirl/issues/milestoneedit.scala.html
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
@(milestone: Option[model.Milestone], repository: service.RepositoryService.RepositoryInfo)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers
|
||||||
|
@html.main("Milestones - " + repository.owner + "/" + repository.name){
|
||||||
|
@html.header("milestones", repository)
|
||||||
|
@issuestab("milestones", repository)
|
||||||
|
<form method="POST" action="@path/@repository.owner/@repository.name/issues/milestones/@if(milestone.isEmpty){new}else{@milestone.get.milestoneId/edit}" validate="true">
|
||||||
|
<fieldset>
|
||||||
|
<label for="title"><string>Title</string></label>
|
||||||
|
<input type="text" id="title" name="title" style="width: 400px;" value="@milestone.map(_.title)"/>
|
||||||
|
<span id="error-title" class="error"></span>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label for="description"><strong>Description</strong></label>
|
||||||
|
<textarea id="description" name="description" style="width: 500px; height: 150px;">@milestone.map(_.description)</textarea>
|
||||||
|
<span id="error-description" class="error"></span>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<label for="dueDate"><strong>Due Date</strong></label>
|
||||||
|
<div id="dueDate" class="input-append date" data-date-format="yyyy-mm-dd" data-date="@milestone.map(_.dueDate.map(helpers.date))">
|
||||||
|
<input class="span2" name="dueDate" type="text" readonly="" value="@milestone.map(_.dueDate.map(helpers.date))" size="16" name="dueDate"/>
|
||||||
|
<span class="add-on"><i class="icon-calendar"></i></span>
|
||||||
|
</div>
|
||||||
|
<span id="error-dueDate" class="error"></span>
|
||||||
|
</fieldset>
|
||||||
|
<hr>
|
||||||
|
<div class="pull-right">
|
||||||
|
@if(milestone.isEmpty){
|
||||||
|
<input type="submit" class="btn" value="Create milestone"/>
|
||||||
|
} else {
|
||||||
|
@if(milestone.get.closed){
|
||||||
|
<input type="button" class="btn" value="Open" id="open"
|
||||||
|
onclick="location.href='@path/@repository.owner/@repository.name/issues/milestones/@milestone.get.milestoneId/close';"/>
|
||||||
|
} else {
|
||||||
|
<input type="button" class="btn" value="Close" id="close"
|
||||||
|
onclick="location.href='@path/@repository.owner/@repository.name/issues/milestones/@milestone.get.milestoneId/open';"/>
|
||||||
|
}
|
||||||
|
<input type="submit" class="btn" value="Update milestone"/>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('#dueDate').datepicker();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
120
src/main/twirl/issues/milestones.scala.html
Normal file
120
src/main/twirl/issues/milestones.scala.html
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
@(state: String, milestones: List[model.Milestone], repository: service.RepositoryService.RepositoryInfo, isWritable: Boolean)(implicit context: app.Context)
|
||||||
|
@import context._
|
||||||
|
@import view.helpers
|
||||||
|
@html.main("Milestones - " + repository.owner + "/" + repository.name){
|
||||||
|
@html.header("milestones", repository)
|
||||||
|
@issuestab("milestones", repository)
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span3">
|
||||||
|
<ul class="nav nav-pills nav-stacked">
|
||||||
|
<li@if(state == "open"){ class="active"}>
|
||||||
|
<a href="?state=open">
|
||||||
|
<span style="float: right; font-weight: bold;">@milestones.filter(!_.closed).size</span>
|
||||||
|
Open Milestones
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li@if(state == "closed"){ class="active"}>
|
||||||
|
<a href="?state=closed">
|
||||||
|
<span style="float: right; font-weight: bold;">@milestones.filter(_.closed).size</span>
|
||||||
|
Closed Milestones
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
@if(isWritable){
|
||||||
|
<hr>
|
||||||
|
<a href="@path/@repository.owner/@repository.name/issues/milestones/new" class="btn" style="display: block;">Create a new milestone</a>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="span9">
|
||||||
|
<table class="table table-bordered table-hover">
|
||||||
|
@defining(milestones.filter(milestone => if(state == "open") !milestone.closed else milestone.closed)){ milestones =>
|
||||||
|
@milestones.map { milestone =>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="milestone row-fluid">
|
||||||
|
<div class="span4">
|
||||||
|
<a href="#" class="milestone-title">@milestone.title</a><br>
|
||||||
|
@if(milestone.dueDate.isDefined){
|
||||||
|
<span class="description">@helpers.date(milestone.dueDate.get)</span>
|
||||||
|
} else {
|
||||||
|
<span class="description">No due date</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="span8">
|
||||||
|
<div class="milestone-menu">
|
||||||
|
<div class="pull-right">
|
||||||
|
@if(isWritable){
|
||||||
|
<a href="@path/@repository.owner/@repository.name/issues/milestones/@milestone.milestoneId/edit">Edit
|
||||||
|
@if(milestone.closed){
|
||||||
|
<a href="@path/@repository.owner/@repository.name/issues/milestones/@milestone.milestoneId/open">Open</a>
|
||||||
|
} else {
|
||||||
|
<a href="@path/@repository.owner/@repository.name/issues/milestones/@milestone.milestoneId/close">Close</a>
|
||||||
|
}
|
||||||
|
<a href="" class="delete">Delete</a>
|
||||||
|
}
|
||||||
|
<a href="">Browse issues</a>
|
||||||
|
</div>
|
||||||
|
<span class="description">0 closed - 0 open</span>
|
||||||
|
</div>
|
||||||
|
<div class="milestone-progress">10%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if(milestone.description.isDefined){
|
||||||
|
<div class="milestone-description">
|
||||||
|
@helpers.markdown(milestone.description.get, repository, false, false, false)
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
@if(milestones.isEmpty){
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 20px; background-color: #eee; text-align: center;">
|
||||||
|
No milestones to show.
|
||||||
|
@if(isWritable){
|
||||||
|
<a href="@path/@repository.owner/@repository.name/issues/milestones/new">Create a new milestone.</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<style type="text/css">
|
||||||
|
a.milestone-title {
|
||||||
|
font-size: 120%;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
div.milestone-description {
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.milestone-menu {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.milestone-menu a {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.milestone-menu a.delete {
|
||||||
|
color: #b00;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.milestone-progress {
|
||||||
|
color: white;
|
||||||
|
padding-left: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
text-shadow: 0px 0px 5px #444;
|
||||||
|
background-color: silver;
|
||||||
|
border-radius: 4px;
|
||||||
|
-webkit-border-radius: 4px;
|
||||||
|
-moz-border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user