mirror of
https://github.com/gitbucket/gitbucket.git
synced 2025-11-11 16:05:49 +01:00
Priorities CRUD interface implemented.
This commit is contained in:
@@ -44,6 +44,7 @@ class ScalatraBootstrap extends LifeCycle with SystemSettingsService {
|
|||||||
context.mount(new RepositoryViewerController, "/*")
|
context.mount(new RepositoryViewerController, "/*")
|
||||||
context.mount(new WikiController, "/*")
|
context.mount(new WikiController, "/*")
|
||||||
context.mount(new LabelsController, "/*")
|
context.mount(new LabelsController, "/*")
|
||||||
|
context.mount(new PrioritiesController, "/*")
|
||||||
context.mount(new MilestonesController, "/*")
|
context.mount(new MilestonesController, "/*")
|
||||||
context.mount(new IssuesController, "/*")
|
context.mount(new IssuesController, "/*")
|
||||||
context.mount(new PullRequestsController, "/*")
|
context.mount(new PullRequestsController, "/*")
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package gitbucket.core.controller
|
||||||
|
|
||||||
|
import gitbucket.core.issues.priorities.html
|
||||||
|
import gitbucket.core.service.{RepositoryService, AccountService, IssuesService, PrioritiesService}
|
||||||
|
import gitbucket.core.util.{ReferrerAuthenticator, WritableUsersAuthenticator}
|
||||||
|
import gitbucket.core.util.Implicits._
|
||||||
|
import io.github.gitbucket.scalatra.forms._
|
||||||
|
import org.scalatra.i18n.Messages
|
||||||
|
import org.scalatra.Ok
|
||||||
|
|
||||||
|
class PrioritiesController extends PrioritiesControllerBase
|
||||||
|
with PrioritiesService with IssuesService with RepositoryService with AccountService
|
||||||
|
with ReferrerAuthenticator with WritableUsersAuthenticator
|
||||||
|
|
||||||
|
trait PrioritiesControllerBase extends ControllerBase {
|
||||||
|
self: PrioritiesService with IssuesService with RepositoryService
|
||||||
|
with ReferrerAuthenticator with WritableUsersAuthenticator =>
|
||||||
|
|
||||||
|
case class PriorityForm(priorityName: String, color: String)
|
||||||
|
|
||||||
|
val priorityForm = mapping(
|
||||||
|
"priorityName" -> trim(label("Priority name", text(required, priorityName, uniquePriorityName, maxlength(100)))),
|
||||||
|
"priorityColor" -> trim(label("Color", text(required, color)))
|
||||||
|
)(PriorityForm.apply)
|
||||||
|
|
||||||
|
|
||||||
|
get("/:owner/:repository/issues/priorities")(referrersOnly { repository =>
|
||||||
|
html.list(
|
||||||
|
getPriorities(repository.owner, repository.name),
|
||||||
|
Map.empty, // TODO
|
||||||
|
repository,
|
||||||
|
hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/issues/priorities/new")(writableUsersOnly { repository =>
|
||||||
|
html.edit(None, repository)
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/priorities/new", priorityForm)(writableUsersOnly { (form, repository) =>
|
||||||
|
val priorityId = createPriority(repository.owner, repository.name, form.priorityName, form.color.substring(1))
|
||||||
|
html.priority(
|
||||||
|
getPriority(repository.owner, repository.name, priorityId).get,
|
||||||
|
Map.empty, // TODO,
|
||||||
|
repository,
|
||||||
|
hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxGet("/:owner/:repository/issues/priorities/:priorityId/edit")(writableUsersOnly { repository =>
|
||||||
|
getPriority(repository.owner, repository.name, params("priorityId").toInt).map { priority =>
|
||||||
|
html.edit(Some(priority), repository)
|
||||||
|
} getOrElse NotFound()
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/priorities/:priorityId/edit", priorityForm)(writableUsersOnly { (form, repository) =>
|
||||||
|
updatePriority(repository.owner, repository.name, params("priorityId").toInt, form.priorityName, form.color.substring(1))
|
||||||
|
html.priority(
|
||||||
|
getPriority(repository.owner, repository.name, params("priorityId").toInt).get,
|
||||||
|
Map.empty, // TODO,
|
||||||
|
repository,
|
||||||
|
hasDeveloperRole(repository.owner, repository.name, context.loginAccount))
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/priorities/reorder")(writableUsersOnly { (repository) =>
|
||||||
|
reorderPriorities(repository.owner, repository.name, params("order")
|
||||||
|
.split(",")
|
||||||
|
.map(id => id.toInt)
|
||||||
|
.zipWithIndex
|
||||||
|
.toMap)
|
||||||
|
|
||||||
|
Ok()
|
||||||
|
})
|
||||||
|
|
||||||
|
ajaxPost("/:owner/:repository/issues/priorities/:priorityId/delete")(writableUsersOnly { repository =>
|
||||||
|
deletePriority(repository.owner, repository.name, params("priorityId").toInt)
|
||||||
|
Ok()
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constraint for the identifier such as user name, repository name or page name.
|
||||||
|
*/
|
||||||
|
private def priorityName: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, messages: Messages): Option[String] =
|
||||||
|
if(value.contains(',')){
|
||||||
|
Some(s"${name} contains invalid character.")
|
||||||
|
} else if(value.startsWith("_") || value.startsWith("-")){
|
||||||
|
Some(s"${name} starts with invalid character.")
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def uniquePriorityName: Constraint = new Constraint(){
|
||||||
|
override def validate(name: String, value: String, params: Map[String, String], messages: Messages): Option[String] = {
|
||||||
|
val owner = params("owner")
|
||||||
|
val repository = params("repository")
|
||||||
|
params.get("priorityId").map { priorityId =>
|
||||||
|
getPriority(owner, repository, value).filter(_.priorityId != priorityId.toInt).map(_ => "Name has already been taken.")
|
||||||
|
}.getOrElse {
|
||||||
|
getPriority(owner, repository, value).map(_ => "Name has already been taken.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package gitbucket.core.service
|
||||||
|
|
||||||
|
import gitbucket.core.model.Priority
|
||||||
|
import gitbucket.core.model.Profile._
|
||||||
|
import gitbucket.core.model.Profile.profile.blockingApi._
|
||||||
|
|
||||||
|
trait PrioritiesService {
|
||||||
|
|
||||||
|
def getPriorities(owner: String, repository: String)(implicit s: Session): List[Priority] =
|
||||||
|
Priorities.filter(_.byRepository(owner, repository)).sortBy(_.ordering asc).list
|
||||||
|
|
||||||
|
def getPriority(owner: String, repository: String, priorityId: Int)(implicit s: Session): Option[Priority] =
|
||||||
|
Priorities.filter(_.byPrimaryKey(owner, repository, priorityId)).firstOption
|
||||||
|
|
||||||
|
def getPriority(owner: String, repository: String, priorityName: String)(implicit s: Session): Option[Priority] =
|
||||||
|
Priorities.filter(_.byPriority(owner, repository, priorityName)).firstOption
|
||||||
|
|
||||||
|
def createPriority(owner: String, repository: String, priorityName: String, color: String)(implicit s: Session): Int = {
|
||||||
|
val ordering = Priorities.filter(_.byRepository(owner, repository))
|
||||||
|
.list
|
||||||
|
.map(p => p.ordering)
|
||||||
|
.reduceOption(_ max _)
|
||||||
|
.map(m => m + 1)
|
||||||
|
.getOrElse(0)
|
||||||
|
|
||||||
|
Priorities returning Priorities.map(_.priorityId) insert Priority(
|
||||||
|
userName = owner,
|
||||||
|
repositoryName = repository,
|
||||||
|
priorityName = priorityName,
|
||||||
|
ordering = ordering,
|
||||||
|
color = color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def updatePriority(owner: String, repository: String, priorityId: Int, priorityName: String, color: String)
|
||||||
|
(implicit s: Session): Unit =
|
||||||
|
Priorities.filter(_.byPrimaryKey(owner, repository, priorityId))
|
||||||
|
.map(t => (t.priorityName, t.color))
|
||||||
|
.update(priorityName, color)
|
||||||
|
|
||||||
|
def reorderPriorities(owner: String, repository: String, order: Map[Int, Int])
|
||||||
|
(implicit s: Session): Unit = {
|
||||||
|
|
||||||
|
Priorities.filter(_.byRepository(owner, repository))
|
||||||
|
.list
|
||||||
|
.foreach(p => Priorities
|
||||||
|
.filter(_.byPrimaryKey(owner, repository, p.priorityId))
|
||||||
|
.map(_.ordering)
|
||||||
|
.update(order.get(p.priorityId).get))
|
||||||
|
}
|
||||||
|
|
||||||
|
def deletePriority(owner: String, repository: String, priorityId: Int)(implicit s: Session): Unit = {
|
||||||
|
// TODO update affected issues
|
||||||
|
Priorities.filter(_.byPrimaryKey(owner, repository, priorityId)).delete
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
@(priority: Option[gitbucket.core.model.Priority],
|
||||||
|
repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context)
|
||||||
|
@import gitbucket.core.view.helpers
|
||||||
|
@defining(priority.map(_.priorityId).getOrElse("new")){ priorityId =>
|
||||||
|
<div id="edit-priority-area-@priorityId">
|
||||||
|
<form class="form-inline">
|
||||||
|
<input type="text" id="priorityName-@priorityId" style="width: 300px; float: left; margin-right: 4px;" class="form-control" value="@priority.map(_.priorityName)"@if(priorityId == "new"){ placeholder="New priority name"}/>
|
||||||
|
<div id="priority-color-@priorityId" class="input-group color bscp" data-color="#@priority.map(_.color).getOrElse("888888")" data-color-format="hex" style="width: 100px; float: left;">
|
||||||
|
<input type="text" class="form-control" id="priorityColor-@priorityId" value="#@priority.map(_.color).getOrElse("888888")" style="width: 100px;">
|
||||||
|
<span class="input-group-addon"><i style="background-color: #@priority.map(_.color).getOrElse("888888");"></i></span>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$('div#priority-color-@priorityId').colorpicker({format: "hex"});
|
||||||
|
</script>
|
||||||
|
<span class="pull-right">
|
||||||
|
<span id="priority-error-@priorityId" class="error"></span>
|
||||||
|
<input type="button" id="cancel-@priorityId" class="btn btn-default priority-edit-cancel" value="Cancel">
|
||||||
|
<input type="button" id="submit-@priorityId" class="btn btn-success" style="margin-bottom: 0px;" value="@(if(priorityId == "new") "Create priority" else "Save changes")"/>
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('#submit-@priorityId').click(function(e){
|
||||||
|
$.post('@helpers.url(repository)/issues/priorities/@{if(priorityId == "new") "new" else priorityId + "/edit"}', {
|
||||||
|
'priorityName' : $('#priorityName-@priorityId').val(),
|
||||||
|
'priorityColor': $('#priorityColor-@priorityId').val()
|
||||||
|
}, function(data, status){
|
||||||
|
$('div#edit-priority-area-@priorityId').remove();
|
||||||
|
@if(priorityId == "new"){
|
||||||
|
$('#new-priority-table').hide();
|
||||||
|
// Insert row into the top of table
|
||||||
|
$('#priorities-table tbody').append(data);
|
||||||
|
} else {
|
||||||
|
// Replace table row
|
||||||
|
$('#priority-row-@priorityId').after(data).remove();
|
||||||
|
}
|
||||||
|
$('#priority-no-priorities').hide();
|
||||||
|
updatePriorityCount();
|
||||||
|
}).fail(function(xhr, status, error){
|
||||||
|
var errors = JSON.parse(xhr.responseText);
|
||||||
|
if(errors.priorityName){
|
||||||
|
$('span#priority-error-@priorityId').text(errors.priorityName);
|
||||||
|
} else if(errors.priorityColor){
|
||||||
|
$('span#priority-error-@priorityId').text(errors.priorityColor);
|
||||||
|
} else {
|
||||||
|
$('span#priority-error-@priorityId').text('error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#cancel-@priorityId').click(function(e){
|
||||||
|
$('div#edit-priority-area-@priorityId').remove();
|
||||||
|
@if(priorityId == "new"){
|
||||||
|
$('#new-priority-table').hide();
|
||||||
|
} else {
|
||||||
|
$('#priority-@priorityId').show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
}
|
||||||
104
src/main/twirl/gitbucket/core/issues/priorities/list.scala.html
Normal file
104
src/main/twirl/gitbucket/core/issues/priorities/list.scala.html
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
@(priorities: List[gitbucket.core.model.Priority],
|
||||||
|
counts: Map[String, Int],
|
||||||
|
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||||
|
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||||
|
@import gitbucket.core.view.helpers
|
||||||
|
@gitbucket.core.html.main(s"Priorities - ${repository.owner}/${repository.name}"){
|
||||||
|
@gitbucket.core.html.menu("priorities", repository){
|
||||||
|
@if(hasWritePermission){
|
||||||
|
<div class="pull-right" style="margin-bottom: 10px;">
|
||||||
|
<a class="btn btn-success" href="javascript:void(0);" id="new-priority-button">New priority</a>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<table class="table table-bordered table-hover table-issues" id="new-priority-table" style="display: none;">
|
||||||
|
<tr><td></td></tr>
|
||||||
|
</table>
|
||||||
|
<table id="priorities-table" class="table table-bordered table-hover table-issues">
|
||||||
|
<thead>
|
||||||
|
<tr id="priority-row-header">
|
||||||
|
<th style="background-color: #eee;">
|
||||||
|
<span class="small">@priorities.size priorities</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@priorities.map { priority =>
|
||||||
|
@gitbucket.core.issues.priorities.html.priority(priority, counts, repository, hasWritePermission)
|
||||||
|
}
|
||||||
|
<tr id="priority-no-priorities" @if(!priorities.isEmpty){ style="display:none" }>
|
||||||
|
<td style="padding: 20px; background-color: #eee; text-align: center;">
|
||||||
|
No priorities to show.
|
||||||
|
@if(hasWritePermission){
|
||||||
|
Click on the "New priority" button above to create one.
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<script>
|
||||||
|
$(function(){
|
||||||
|
$('#new-priority-button').click(function(e){
|
||||||
|
if($('#edit-priority-area-new').size() != 0){
|
||||||
|
$('div#edit-priority-area-new').remove();
|
||||||
|
$('#new-priority-table').hide();
|
||||||
|
} else {
|
||||||
|
$.get('@helpers.url(repository)/issues/priorities/new',
|
||||||
|
function(data){
|
||||||
|
$('#new-priority-table').show().find('tr td').append(data);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@if(hasWritePermission){
|
||||||
|
$('#priorities-table tbody').sortable({
|
||||||
|
axis: 'y',
|
||||||
|
cursor: 'move',
|
||||||
|
helper: function (e, ui) {
|
||||||
|
ui.children().each(function() {
|
||||||
|
$(this).width($(this).width());
|
||||||
|
});
|
||||||
|
return ui;
|
||||||
|
},
|
||||||
|
handle: '.priority-sort-handle',
|
||||||
|
update: function() {
|
||||||
|
var ids = [];
|
||||||
|
$('tr.priority-row').each(function(idx, elem) {
|
||||||
|
ids.push($(elem).attr('id').replace('priority-row-', ''));
|
||||||
|
});
|
||||||
|
$.post('@helpers.url(repository)/issues/priorities/reorder', {
|
||||||
|
'order' : ids.join(',')
|
||||||
|
}).fail(function(xhr, status, error){
|
||||||
|
alert('Unable to reorder priorities.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function deletePriority(priorityId){
|
||||||
|
if(confirm('Once you delete this priority, there is no going back.\nAre you sure?')){
|
||||||
|
$.post('@helpers.url(repository)/issues/priorities/' + priorityId + '/delete', function(){
|
||||||
|
$('tr#priority-row-' + priorityId).remove();
|
||||||
|
if ($('tr.priority-row').size() == 0) {
|
||||||
|
$('#priority-no-priorities').show();
|
||||||
|
}
|
||||||
|
updatePriorityCount();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function editPriority(priorityId){
|
||||||
|
$.get('@helpers.url(repository)/issues/priorities/' + priorityId + '/edit',
|
||||||
|
function(data){
|
||||||
|
$('#priority-' + priorityId).hide().parent().append(data);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePriorityCount() {
|
||||||
|
var $counter = $('#priority-row-header span');
|
||||||
|
$counter.text($counter.text().replace(/\d+/, $('tr.priority-row').size()));
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
@(priority: gitbucket.core.model.Priority,
|
||||||
|
counts: Map[String, Int],
|
||||||
|
repository: gitbucket.core.service.RepositoryService.RepositoryInfo,
|
||||||
|
hasWritePermission: Boolean)(implicit context: gitbucket.core.controller.Context)
|
||||||
|
@import gitbucket.core.view.helpers
|
||||||
|
<tr id="priority-row-@priority.priorityId" class="priority-row">
|
||||||
|
<td style="padding-top: 15px; padding-bottom: 15px;">
|
||||||
|
<div class="milestone row" id="priority-@priority.priorityId">
|
||||||
|
<div class="col-md-8">
|
||||||
|
@if(hasWritePermission) {
|
||||||
|
<div class="pull-left priority-sort-handle" style="margin-top: 3px"><i class="octicon octicon-grabber" style="font-size: 2em"></i></div>
|
||||||
|
}
|
||||||
|
<div style="margin-top: 6px">
|
||||||
|
<a href="@helpers.url(repository)/issues?priority=@helpers.urlEncode(priority.priorityName)&state=open" id="priority-row-content-@priority.priorityId">
|
||||||
|
<span style="background-color: #@priority.color; color: #@priority.fontColor; padding: 8px; font-size: 120%; border-radius: 4px;">
|
||||||
|
<i class="octicon octicon-flame" style="color: #@priority.fontColor;"></i>
|
||||||
|
@priority.priorityName
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="@if(hasWritePermission){col-md-2} else {col-md-4}">
|
||||||
|
<div class="pull-right">
|
||||||
|
<span class="muted">@counts.get(priority.priorityName).getOrElse(0) open issues</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@if(hasWritePermission){
|
||||||
|
<div class="col-md-2">
|
||||||
|
<div class="pull-right">
|
||||||
|
<a href="javascript:void(0);" onclick="editPriority(@priority.priorityId)">Edit</a>
|
||||||
|
|
||||||
|
<a href="javascript:void(0);" onclick="deletePriority(@priority.priorityId)">Delete</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
@menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount)
|
@menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount)
|
||||||
@menuitem("/pulls", "pulls", "Pull requests", "git-pull-request", repository.pullCount)
|
@menuitem("/pulls", "pulls", "Pull requests", "git-pull-request", repository.pullCount)
|
||||||
@menuitem("/issues/labels", "labels", "Labels", "tag")
|
@menuitem("/issues/labels", "labels", "Labels", "tag")
|
||||||
|
@menuitem("/issues/priorities", "priorities", "Priorities", "flame")
|
||||||
@menuitem("/issues/milestones", "milestones", "Milestones", "milestone")
|
@menuitem("/issues/milestones", "milestones", "Milestones", "milestone")
|
||||||
} else {
|
} else {
|
||||||
@repository.repository.options.externalIssuesUrl.map { externalIssuesUrl =>
|
@repository.repository.options.externalIssuesUrl.map { externalIssuesUrl =>
|
||||||
|
|||||||
@@ -943,6 +943,18 @@ pre.reset.discussion-item-content-text{
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.priority-sort-handle {
|
||||||
|
margin-top: 3px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priority-sort-handle i::before {
|
||||||
|
cursor: move;
|
||||||
|
display: block;
|
||||||
|
width: 0.5em;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/****************************************************************************/
|
/****************************************************************************/
|
||||||
/* Pull request */
|
/* Pull request */
|
||||||
/****************************************************************************/
|
/****************************************************************************/
|
||||||
|
|||||||
Reference in New Issue
Block a user