diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index 949bf80..f9be729 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -44,6 +44,7 @@ context.mount(new RepositoryViewerController, "/*") context.mount(new WikiController, "/*") context.mount(new LabelsController, "/*") + context.mount(new PrioritiesController, "/*") context.mount(new MilestonesController, "/*") context.mount(new IssuesController, "/*") context.mount(new PullRequestsController, "/*") diff --git a/src/main/scala/gitbucket/core/controller/PrioritiesController.scala b/src/main/scala/gitbucket/core/controller/PrioritiesController.scala new file mode 100644 index 0000000..b63ee48 --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/PrioritiesController.scala @@ -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.") + } + } + } +} diff --git a/src/main/scala/gitbucket/core/service/PrioritiesService.scala b/src/main/scala/gitbucket/core/service/PrioritiesService.scala new file mode 100644 index 0000000..de57027 --- /dev/null +++ b/src/main/scala/gitbucket/core/service/PrioritiesService.scala @@ -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 + } +} diff --git a/src/main/twirl/gitbucket/core/issues/priorities/edit.scala.html b/src/main/twirl/gitbucket/core/issues/priorities/edit.scala.html new file mode 100644 index 0000000..d3c49b7 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/priorities/edit.scala.html @@ -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 => +
+
+ +
+ + +
+ + + + + + +
+
+ +} diff --git a/src/main/twirl/gitbucket/core/issues/priorities/list.scala.html b/src/main/twirl/gitbucket/core/issues/priorities/list.scala.html new file mode 100644 index 0000000..4ce7232 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/priorities/list.scala.html @@ -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){ +
+ New priority +
+ } + + + + + + + + + + + @priorities.map { priority => + @gitbucket.core.issues.priorities.html.priority(priority, counts, repository, hasWritePermission) + } + + + + +
+ @priorities.size priorities +
+ } +} + diff --git a/src/main/twirl/gitbucket/core/issues/priorities/priority.scala.html b/src/main/twirl/gitbucket/core/issues/priorities/priority.scala.html new file mode 100644 index 0000000..ea71bee --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/priorities/priority.scala.html @@ -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 + + +
+
+ @if(hasWritePermission) { +
+ } +
+ + + + @priority.priorityName + + +
+
+
+
+ @counts.get(priority.priorityName).getOrElse(0) open issues +
+
+ @if(hasWritePermission){ +
+
+ Edit +    + Delete +
+
+ } +
+ + diff --git a/src/main/twirl/gitbucket/core/menu.scala.html b/src/main/twirl/gitbucket/core/menu.scala.html index 5ee9370..c6f5f70 100644 --- a/src/main/twirl/gitbucket/core/menu.scala.html +++ b/src/main/twirl/gitbucket/core/menu.scala.html @@ -39,6 +39,7 @@ @menuitem("/issues", "issues", "Issues", "issue-opened", repository.issueCount) @menuitem("/pulls", "pulls", "Pull requests", "git-pull-request", repository.pullCount) @menuitem("/issues/labels", "labels", "Labels", "tag") + @menuitem("/issues/priorities", "priorities", "Priorities", "flame") @menuitem("/issues/milestones", "milestones", "Milestones", "milestone") } else { @repository.repository.options.externalIssuesUrl.map { externalIssuesUrl => diff --git a/src/main/webapp/assets/common/css/gitbucket.css b/src/main/webapp/assets/common/css/gitbucket.css index 4e22054..6b6e4fb 100644 --- a/src/main/webapp/assets/common/css/gitbucket.css +++ b/src/main/webapp/assets/common/css/gitbucket.css @@ -943,6 +943,18 @@ 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 */ /****************************************************************************/