diff --git a/src/main/resources/update/gitbucket-core_4.14.sql b/src/main/resources/update/gitbucket-core_4.14.sql new file mode 100644 index 0000000..1ba0103 --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.14.sql @@ -0,0 +1,26 @@ +CREATE OR REPLACE VIEW ISSUE_OUTLINE_VIEW AS + + SELECT + A.USER_NAME, + A.REPOSITORY_NAME, + A.ISSUE_ID, + COALESCE(B.COMMENT_COUNT, 0) + COALESCE(C.COMMENT_COUNT, 0) AS COMMENT_COUNT, + COALESCE(D.ORDERING, 9999) AS PRIORITY + + FROM ISSUE A + + LEFT OUTER JOIN ( + SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM ISSUE_COMMENT + WHERE ACTION IN ('comment', 'close_comment', 'reopen_comment') + GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID + ) B + ON (A.USER_NAME = B.USER_NAME AND A.REPOSITORY_NAME = B.REPOSITORY_NAME AND A.ISSUE_ID = B.ISSUE_ID) + + LEFT OUTER JOIN ( + SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, COUNT(COMMENT_ID) AS COMMENT_COUNT FROM COMMIT_COMMENT + GROUP BY USER_NAME, REPOSITORY_NAME, ISSUE_ID + ) C + ON (A.USER_NAME = C.USER_NAME AND A.REPOSITORY_NAME = C.REPOSITORY_NAME AND A.ISSUE_ID = C.ISSUE_ID) + + LEFT OUTER JOIN PRIORITY D + ON (A.PRIORITY_ID = D.PRIORITY_ID); diff --git a/src/main/resources/update/gitbucket-core_4.14.xml b/src/main/resources/update/gitbucket-core_4.14.xml new file mode 100644 index 0000000..9f8a9a7 --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.14.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + 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/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index 33adca1..8d678b3 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -34,5 +34,9 @@ ), new Version("4.12.0"), new Version("4.12.1"), - new Version("4.13.0") + new Version("4.13.0"), + new Version("4.14.0", + new LiquibaseMigration("update/gitbucket-core_4.14.xml"), + new SqlMigration("update/gitbucket-core_4.14.sql") + ) ) diff --git a/src/main/scala/gitbucket/core/controller/AccountController.scala b/src/main/scala/gitbucket/core/controller/AccountController.scala index c2235f7..045fbfe 100644 --- a/src/main/scala/gitbucket/core/controller/AccountController.scala +++ b/src/main/scala/gitbucket/core/controller/AccountController.scala @@ -20,13 +20,13 @@ class AccountController extends AccountControllerBase with AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator - with AccessTokenService with WebHookService with RepositoryCreationService + with AccessTokenService with WebHookService with PrioritiesService with RepositoryCreationService trait AccountControllerBase extends AccountManagementControllerBase { self: AccountService with RepositoryService with ActivityService with WikiService with LabelsService with SshKeyService with OneselfAuthenticator with UsersAuthenticator with GroupManagerAuthenticator with ReadableUsersAuthenticator - with AccessTokenService with WebHookService with RepositoryCreationService => + with AccessTokenService with WebHookService with PrioritiesService with RepositoryCreationService => case class AccountNewForm(userName: String, password: String, fullName: String, mailAddress: String, description: Option[String], url: Option[String], fileId: Option[String]) diff --git a/src/main/scala/gitbucket/core/controller/ApiController.scala b/src/main/scala/gitbucket/core/controller/ApiController.scala index e873ae2..9da265e 100644 --- a/src/main/scala/gitbucket/core/controller/ApiController.scala +++ b/src/main/scala/gitbucket/core/controller/ApiController.scala @@ -33,6 +33,7 @@ with WebHookIssueCommentService with WikiService with ActivityService + with PrioritiesService with OwnerAuthenticator with UsersAuthenticator with GroupManagerAuthenticator @@ -52,6 +53,7 @@ with RepositoryCreationService with IssueCreationService with HandleCommentService + with PrioritiesService with OwnerAuthenticator with UsersAuthenticator with GroupManagerAuthenticator @@ -365,6 +367,7 @@ data.body, data.assignees.headOption, milestone.map(_.milestoneId), + None, data.labels, loginAccount) JsonFormat(ApiIssue(issue, RepositoryName(repository), ApiUser(loginAccount))) diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index d149915..9bebbfc 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -27,6 +27,7 @@ with PullRequestService with WebHookIssueCommentService with CommitsService + with PrioritiesService trait IssuesControllerBase extends ControllerBase { self: IssuesService @@ -41,10 +42,11 @@ with ReferrerAuthenticator with WritableUsersAuthenticator with PullRequestService - with WebHookIssueCommentService => + with WebHookIssueCommentService + with PrioritiesService => case class IssueCreateForm(title: String, content: Option[String], - assignedUserName: Option[String], milestoneId: Option[Int], labelNames: Option[String]) + assignedUserName: Option[String], milestoneId: Option[Int], priorityId: Option[Int], labelNames: Option[String]) case class CommentForm(issueId: Int, content: String) case class IssueStateForm(issueId: Int, content: Option[String]) @@ -53,6 +55,7 @@ "content" -> trim(optional(text())), "assignedUserName" -> trim(optional(text())), "milestoneId" -> trim(optional(number())), + "priorityId" -> trim(optional(number())), "labelNames" -> trim(optional(text())) )(IssueCreateForm.apply) @@ -94,6 +97,7 @@ getIssueLabels(owner, name, issueId.toInt), getAssignableUserNames(owner, name), getMilestonesWithIssueCount(owner, name), + getPriorities(owner, name), getLabels(owner, name), isIssueEditable(repository), isIssueManageable(repository), @@ -109,6 +113,8 @@ html.create( getAssignableUserNames(owner, name), getMilestones(owner, name), + getPriorities(owner, name), + getDefaultPriority(owner, name), getLabels(owner, name), isIssueManageable(repository), getContentTemplate(repository, "ISSUE_TEMPLATE"), @@ -125,6 +131,7 @@ form.content, form.assignedUserName, form.milestoneId, + form.priorityId, form.labelNames.toArray.flatMap(_.split(",")), context.loginAccount.get) @@ -291,6 +298,11 @@ } getOrElse Ok() }) + ajaxPost("/:owner/:repository/issues/:id/priority")(writableUsersOnly { repository => + updatePriorityId(repository.owner, repository.name, params("id").toInt, priorityId("priorityId")) + Ok("updated") + }) + post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository => defining(params.get("value")){ action => action match { @@ -335,6 +347,14 @@ } }) + post("/:owner/:repository/issues/batchedit/priority")(writableUsersOnly { repository => + defining(priorityId("value")){ value => + executeBatch(repository) { + updatePriorityId(repository.owner, repository.name, _, value) + } + } + }) + get("/:owner/:repository/_attached/:file")(referrersOnly { repository => (Directory.getAttachedDir(repository.owner, repository.name) match { case dir if(dir.exists && dir.isDirectory) => @@ -348,6 +368,7 @@ val assignedUserName = (key: String) => params.get(key) filter (_.trim != "") val milestoneId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) + val priorityId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) private def executeBatch(repository: RepositoryService.RepositoryInfo)(execute: Int => Unit) = { params("checked").split(',') map(_.toInt) foreach execute @@ -370,6 +391,7 @@ page, getAssignableUserNames(owner, repoName), getMilestones(owner, repoName), + getPriorities(owner, repoName), getLabels(owner, repoName), countIssue(condition.copy(state = "open" ), false, owner -> repoName), countIssue(condition.copy(state = "closed"), false, owner -> repoName), 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..e0e010a --- /dev/null +++ b/src/main/scala/gitbucket/core/controller/PrioritiesController.scala @@ -0,0 +1,111 @@ +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, description: Option[String], color: String) + + val priorityForm = mapping( + "priorityName" -> trim(label("Priority name", text(required, priorityName, uniquePriorityName, maxlength(100)))), + "description" -> trim(label("Description", optional(text(maxlength(255))))), + "priorityColor" -> trim(label("Color", text(required, color))) + )(PriorityForm.apply) + + + get("/:owner/:repository/issues/priorities")(referrersOnly { repository => + html.list( + getPriorities(repository.owner, repository.name), + countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), + 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.description, form.color.substring(1)) + html.priority( + getPriority(repository.owner, repository.name, priorityId).get, + countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), + 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.description, form.color.substring(1)) + html.priority( + getPriority(repository.owner, repository.name, params("priorityId").toInt).get, + countIssueGroupByPriorities(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty), + 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/default")(writableUsersOnly { (repository) => + setDefaultPriority(repository.owner, repository.name, priorityId("priorityId")) + Ok() + }) + + ajaxPost("/:owner/:repository/issues/priorities/:priorityId/delete")(writableUsersOnly { repository => + deletePriority(repository.owner, repository.name, params("priorityId").toInt) + Ok() + }) + + val priorityId: String => Option[Int] = (key: String) => params.get(key).flatMap(_.toIntOpt) + + /** + * 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/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 4029ff0..88328e1 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -24,14 +24,14 @@ with RepositoryService with AccountService with IssuesService with PullRequestService with MilestonesService with LabelsService with CommitsService with ActivityService with WebHookPullRequestService with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator - with CommitStatusService with MergeService with ProtectedBranchService + with CommitStatusService with MergeService with ProtectedBranchService with PrioritiesService trait PullRequestsControllerBase extends ControllerBase { self: RepositoryService with AccountService with IssuesService with MilestonesService with LabelsService with CommitsService with ActivityService with PullRequestService with WebHookPullRequestService with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator - with CommitStatusService with MergeService with ProtectedBranchService => + with CommitStatusService with MergeService with ProtectedBranchService with PrioritiesService => val pullRequestForm = mapping( "title" -> trim(label("Title" , text(required, maxlength(100)))), @@ -45,6 +45,7 @@ "commitIdTo" -> trim(text(required, maxlength(40))), "assignedUserName" -> trim(optional(text())), "milestoneId" -> trim(optional(number())), + "priorityId" -> trim(optional(number())), "labelNames" -> trim(optional(text())) )(PullRequestForm.apply) @@ -64,6 +65,7 @@ commitIdTo: String, assignedUserName: Option[String], milestoneId: Option[Int], + priorityId: Option[Int], labelNames: Option[String] ) @@ -93,6 +95,7 @@ getIssueLabels(owner, name, issueId), getAssignableUserNames(owner, name), getMilestonesWithIssueCount(owner, name), + getPriorities(owner, name), getLabels(owner, name), commits, diffs, @@ -390,6 +393,7 @@ hasDeveloperRole(originRepository.owner, originRepository.name, context.loginAccount), getAssignableUserNames(originRepository.owner, originRepository.name), getMilestones(originRepository.owner, originRepository.name), + getPriorities(originRepository.owner, originRepository.name), getLabels(originRepository.owner, originRepository.name) ) } @@ -445,6 +449,7 @@ content = form.content, assignedUserName = if (manageable) form.assignedUserName else None, milestoneId = if (manageable) form.milestoneId else None, + priorityId = if (manageable) form.priorityId else None, isPullRequest = true) createPullRequest( @@ -518,6 +523,7 @@ page, getAssignableUserNames(owner, repoName), getMilestones(owner, repoName), + getPriorities(owner, repoName), getLabels(owner, repoName), countIssue(condition.copy(state = "open" ), true, owner -> repoName), countIssue(condition.copy(state = "closed"), true, owner -> repoName), diff --git a/src/main/scala/gitbucket/core/model/BasicTemplate.scala b/src/main/scala/gitbucket/core/model/BasicTemplate.scala index c3b8e1b..2ebb28c 100644 --- a/src/main/scala/gitbucket/core/model/BasicTemplate.scala +++ b/src/main/scala/gitbucket/core/model/BasicTemplate.scala @@ -38,6 +38,20 @@ byRepository(owner, repository) && (this.labelName === labelName.bind) } + trait PriorityTemplate extends BasicTemplate { self: Table[_] => + val priorityId = column[Int]("PRIORITY_ID") + val priorityName = column[String]("PRIORITY_NAME") + + def byPriority(owner: String, repository: String, priorityId: Int) = + byRepository(owner, repository) && (this.priorityId === priorityId.bind) + + def byPriority(userName: Rep[String], repositoryName: Rep[String], priorityId: Rep[Int]) = + byRepository(userName, repositoryName) && (this.priorityId === priorityId) + + def byPriority(owner: String, repository: String, priorityName: String) = + byRepository(owner, repository) && (this.priorityName === priorityName.bind) + } + trait MilestoneTemplate extends BasicTemplate { self: Table[_] => val milestoneId = column[Int]("MILESTONE_ID") diff --git a/src/main/scala/gitbucket/core/model/Issue.scala b/src/main/scala/gitbucket/core/model/Issue.scala index fd7a5ce..7167195 100644 --- a/src/main/scala/gitbucket/core/model/Issue.scala +++ b/src/main/scala/gitbucket/core/model/Issue.scala @@ -13,12 +13,13 @@ def byPrimaryKey(owner: String, repository: String) = byRepository(owner, repository) } - class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate { + class IssueOutline(tag: Tag) extends Table[(String, String, Int, Int, Int)](tag, "ISSUE_OUTLINE_VIEW") with IssueTemplate { val commentCount = column[Int]("COMMENT_COUNT") - def * = (userName, repositoryName, issueId, commentCount) + val priority = column[Int]("PRIORITY") + def * = (userName, repositoryName, issueId, commentCount, priority) } - class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate { + class Issues(tag: Tag) extends Table[Issue](tag, "ISSUE") with IssueTemplate with MilestoneTemplate with PriorityTemplate { val openedUserName = column[String]("OPENED_USER_NAME") val assignedUserName = column[String]("ASSIGNED_USER_NAME") val title = column[String]("TITLE") @@ -27,7 +28,7 @@ val registeredDate = column[java.util.Date]("REGISTERED_DATE") val updatedDate = column[java.util.Date]("UPDATED_DATE") val pullRequest = column[Boolean]("PULL_REQUEST") - def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply) + def * = (userName, repositoryName, issueId, openedUserName, milestoneId.?, priorityId.?, assignedUserName.?, title, content.?, closed, registeredDate, updatedDate, pullRequest) <> (Issue.tupled, Issue.unapply) def byPrimaryKey(owner: String, repository: String, issueId: Int) = byIssue(owner, repository, issueId) } @@ -39,6 +40,7 @@ issueId: Int, openedUserName: String, milestoneId: Option[Int], + priorityId: Option[Int], assignedUserName: Option[String], title: String, content: Option[String], diff --git a/src/main/scala/gitbucket/core/model/Priorities.scala b/src/main/scala/gitbucket/core/model/Priorities.scala new file mode 100644 index 0000000..eb31740 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/Priorities.scala @@ -0,0 +1,43 @@ +package gitbucket.core.model + +trait PriorityComponent extends TemplateComponent { self: Profile => + import profile.api._ + + lazy val Priorities = TableQuery[Priorities] + + class Priorities(tag: Tag) extends Table[Priority](tag, "PRIORITY") with PriorityTemplate { + override val priorityId = column[Int]("PRIORITY_ID", O AutoInc) + override val priorityName = column[String]("PRIORITY_NAME") + val description = column[String]("DESCRIPTION") + val ordering = column[Int]("ORDERING") + val isDefault = column[Boolean]("IS_DEFAULT") + val color = column[String]("COLOR") + def * = (userName, repositoryName, priorityId, priorityName, description.?, isDefault, ordering, color) <> (Priority.tupled, Priority.unapply) + + def byPrimaryKey(owner: String, repository: String, priorityId: Int) = byPriority(owner, repository, priorityId) + def byPrimaryKey(userName: Rep[String], repositoryName: Rep[String], priorityId: Rep[Int]) = byPriority(userName, repositoryName, priorityId) + } +} + +case class Priority ( + userName: String, + repositoryName: String, + priorityId: Int = 0, + priorityName: String, + description: Option[String], + isDefault: Boolean, + ordering: Int = 0, + color: String){ + + val fontColor = { + val r = color.substring(0, 2) + val g = color.substring(2, 4) + val b = color.substring(4, 6) + + if(Integer.parseInt(r, 16) + Integer.parseInt(g, 16) + Integer.parseInt(b, 16) > 408){ + "000000" + } else { + "ffffff" + } + } +} diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala index 332e7ea..876188c 100644 --- a/src/main/scala/gitbucket/core/model/Profile.scala +++ b/src/main/scala/gitbucket/core/model/Profile.scala @@ -47,6 +47,7 @@ with IssueCommentComponent with IssueLabelComponent with LabelComponent + with PriorityComponent with MilestoneComponent with PullRequestComponent with RepositoryComponent diff --git a/src/main/scala/gitbucket/core/service/IssueCreationService.scala b/src/main/scala/gitbucket/core/service/IssueCreationService.scala index 09e7dfb..ad67266 100644 --- a/src/main/scala/gitbucket/core/service/IssueCreationService.scala +++ b/src/main/scala/gitbucket/core/service/IssueCreationService.scala @@ -12,7 +12,7 @@ self: RepositoryService with WebHookIssueCommentService with LabelsService with IssuesService with ActivityService => def createIssue(repository: RepositoryInfo, title:String, body:Option[String], - assignee: Option[String], milestoneId: Option[Int], labelNames: Seq[String], + assignee: Option[String], milestoneId: Option[Int], priorityId: Option[Int], labelNames: Seq[String], loginAccount: Account)(implicit context: Context, s: Session) : Issue = { val owner = repository.owner @@ -23,7 +23,8 @@ // insert issue val issueId = insertIssue(owner, name, userName, title, body, if (manageable) assignee else None, - if (manageable) milestoneId else None) + if (manageable) milestoneId else None, + if (manageable) priorityId else None) val issue: Issue = getIssue(owner, name, issueId.toString).get // insert labels diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index 4a9b12c..a782dde 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -97,6 +97,30 @@ .list.toMap } + /** + * Returns the Map which contains issue count for each priority. + * + * @param owner the repository owner + * @param repository the repository name + * @param condition the search condition + * @return the Map which contains issue count for each priority (key is priority name, value is issue count) + */ + def countIssueGroupByPriorities(owner: String, repository: String, condition: IssueSearchCondition, + filterUser: Map[String, String])(implicit s: Session): Map[String, Int] = { + + searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), false) + .join(Priorities).on { case t1 ~ t2 => + t1.byPriority(t2.userName, t2.repositoryName, t2.priorityId) + } + .groupBy { case t1 ~ t2 => + t2.priorityName + } + .map { case priorityName ~ t => + priorityName -> t.length + } + .list.toMap + } + def getCommitStatues(userName: String, repositoryName: String, issueId: Int)(implicit s: Session): Option[CommitStatusInfo] = { val status = PullRequests .filter { pr => @@ -136,21 +160,23 @@ (implicit s: Session): List[IssueInfo] = { // get issues and comment count and labels val result = searchIssueQueryBase(condition, pullRequest, offset, limit, repos) - .joinLeft (IssueLabels) .on { case t1 ~ t2 ~ i ~ t3 => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } - .joinLeft (Labels) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t3.map(_.byLabel(t4.userName, t4.repositoryName, t4.labelId)) } - .joinLeft (Milestones) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } - .sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => i asc } - .map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => - (t1, t2.commentCount, t4.map(_.labelId), t4.map(_.labelName), t4.map(_.color), t5.map(_.title)) + .joinLeft (IssueLabels) .on { case t1 ~ t2 ~ i ~ t3 => t1.byIssue(t3.userName, t3.repositoryName, t3.issueId) } + .joinLeft (Labels) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t3.map(_.byLabel(t4.userName, t4.repositoryName, t4.labelId)) } + .joinLeft (Milestones) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t1.byMilestone(t5.userName, t5.repositoryName, t5.milestoneId) } + .joinLeft (Priorities) .on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t1.byPriority(t6.userName, t6.repositoryName, t6.priorityId) } + .sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => i asc } + .map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => + (t1, t2.commentCount, t4.map(_.labelId), t4.map(_.labelName), t4.map(_.color), t5.map(_.title), t6.map(_.priorityName)) } .list .splitWith { (c1, c2) => c1._1.userName == c2._1.userName && c1._1.repositoryName == c2._1.repositoryName && c1._1.issueId == c2._1.issueId } result.map { issues => issues.head match { - case (issue, commentCount, _, _, _, milestone) => + case (issue, commentCount, _, _, _, milestone, priority) => IssueInfo(issue, issues.flatMap { t => t._3.map (Label(issue.userName, issue.repositoryName, _, t._4.get, t._5.get))} toList, milestone, + priority, commentCount, getCommitStatues(issue.userName, issue.repositoryName, issue.issueId)) }} toList @@ -204,6 +230,10 @@ case "asc" => t1.updatedDate asc case "desc" => t1.updatedDate desc } + case "priority" => condition.direction match { + case "asc" => t2.priority asc + case "desc" => t2.priority desc + } } } .drop(offset).take(limit).zipWithIndex @@ -219,6 +249,7 @@ .foldLeft[Rep[Boolean]](false) ( _ || _ ) && (t1.closed === (condition.state == "closed").bind) && (t1.milestoneId.? isEmpty, condition.milestone == Some(None)) && + (t1.priorityId.? isEmpty, condition.priority == Some(None)) && (t1.assignedUserName.? isEmpty, condition.assigned == Some(None)) && (t1.openedUserName === condition.author.get.bind, condition.author.isDefined) && (t1.pullRequest === pullRequest.bind) && @@ -227,6 +258,11 @@ (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.milestoneId)) && (t2.title === condition.milestone.get.get.bind) } exists, condition.milestone.flatten.isDefined) && + // Priority filter + (Priorities filter { t2 => + (t2.byPrimaryKey(t1.userName, t1.repositoryName, t1.priorityId)) && + (t2.priorityName === condition.priority.get.get.bind) + } exists, condition.priority.flatten.isDefined) && // Assignee filter (t1.assignedUserName === condition.assigned.get.get.bind, condition.assigned.flatten.isDefined) && // Label filter @@ -253,7 +289,7 @@ } def insertIssue(owner: String, repository: String, loginUser: String, title: String, content: Option[String], - assignedUserName: Option[String], milestoneId: Option[Int], + assignedUserName: Option[String], milestoneId: Option[Int], priorityId: Option[Int], isPullRequest: Boolean = false)(implicit s: Session): Int = { // next id number sql"SELECT ISSUE_ID + 1 FROM ISSUE_ID WHERE USER_NAME = $owner AND REPOSITORY_NAME = $repository FOR UPDATE".as[Int] @@ -264,6 +300,7 @@ id, loginUser, milestoneId, + priorityId, assignedUserName, title, content, @@ -316,6 +353,10 @@ Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.milestoneId?).update (milestoneId) } + def updatePriorityId(owner: String, repository: String, issueId: Int, priorityId: Option[Int])(implicit s: Session): Int = { + Issues.filter (_.byPrimaryKey(owner, repository, issueId)).map(_.priorityId?).update (priorityId) + } + def updateComment(commentId: Int, content: String)(implicit s: Session): Int = { IssueComments.filter (_.byPrimaryKey(commentId)).map(t => (t.content, t.updatedDate)).update(content, currentDate) } @@ -430,6 +471,7 @@ case class IssueSearchCondition( labels: Set[String] = Set.empty, milestone: Option[Option[String]] = None, + priority: Option[Option[String]] = None, author: Option[String] = None, assigned: Option[Option[String]] = None, mentioned: Option[String] = None, @@ -459,6 +501,10 @@ case Some(x) => s"milestone:${x}" case None => "no:milestone" }}, + priority.map { _ match { + case Some(x) => s"priority:${x}" + case None => "no:priority" + }}, (sort, direction) match { case ("created" , "desc") => None case ("created" , "asc" ) => Some("sort:created-asc") @@ -466,6 +512,8 @@ case ("comments", "asc" ) => Some("sort:comments-asc") case ("updated" , "desc") => Some("sort:updated-desc") case ("updated" , "asc" ) => Some("sort:updated-asc") + case ("priority", "desc") => Some("sort:priority-desc") + case ("priority", "asc" ) => Some("sort:priority-asc") case x => throw new MatchError(x) }, visibility.map(visibility => s"visibility:${visibility}") @@ -480,6 +528,10 @@ case Some(x) => "milestone=" + urlEncode(x) case None => "milestone=none" }, + priority.map { + case Some(x) => "priority=" + urlEncode(x) + case None => "priority=none" + }, author .map(x => "author=" + urlEncode(x)), assigned.map { case Some(x) => "assigned=" + urlEncode(x) @@ -512,6 +564,10 @@ case "none" => None case x => Some(x) }, + param(request, "priority").map { + case "none" => None + case x => Some(x) + }, param(request, "author"), param(request, "assigned").map { case "none" => None @@ -519,7 +575,7 @@ }, param(request, "mentioned"), param(request, "state", Seq("open", "closed")).getOrElse("open"), - param(request, "sort", Seq("created", "comments", "updated")).getOrElse("created"), + param(request, "sort", Seq("created", "comments", "updated", "priority")).getOrElse("created"), param(request, "direction", Seq("asc", "desc")).getOrElse("desc"), param(request, "visibility"), param(request, "groups").map(_.split(",").toSet).getOrElse(Set.empty) @@ -535,6 +591,6 @@ case class CommitStatusInfo(count: Int, successCount: Int, context: Option[String], state: Option[CommitState], targetUrl: Option[String], description: Option[String]) - case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], commentCount: Int, status:Option[CommitStatusInfo]) + case class IssueInfo(issue: Issue, labels: List[Label], milestone: Option[String], priority: Option[String], commentCount: Int, status:Option[CommitStatusInfo]) } 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..cafff4b --- /dev/null +++ b/src/main/scala/gitbucket/core/service/PrioritiesService.scala @@ -0,0 +1,84 @@ +package gitbucket.core.service + +import gitbucket.core.model.Priority +import gitbucket.core.model.Profile._ +import gitbucket.core.model.Profile.profile.blockingApi._ +import gitbucket.core.util.StringUtil + +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, description: Option[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, + description = description, + isDefault = false, + ordering = ordering, + color = color + ) + } + + def updatePriority(owner: String, repository: String, priorityId: Int, priorityName: String, description: Option[String], color: String) + (implicit s: Session): Unit = + Priorities.filter(_.byPrimaryKey(owner, repository, priorityId)) + .map(t => (t.priorityName, t.description.?, t.color)) + .update(priorityName, description, 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 = { + Issues.filter(_.byRepository(owner, repository)) + .filter(_.priorityId === priorityId) + .map(_.priorityId?) + .update(None) + + Priorities.filter(_.byPrimaryKey(owner, repository, priorityId)).delete + } + + def getDefaultPriority(owner: String, repository: String)(implicit s: Session): Option[Priority] = { + Priorities + .filter(_.byRepository(owner, repository)) + .filter(_.isDefault) + .list + .headOption + } + + def setDefaultPriority(owner: String, repository: String, priorityId: Option[Int])(implicit s: Session): Unit = { + Priorities + .filter(_.byRepository(owner, repository)) + .filter(_.isDefault) + .map(_.isDefault) + .update(false) + + priorityId.foreach(id => Priorities + .filter(_.byPrimaryKey(owner, repository, id)) + .map(_.isDefault) + .update(true)) + } +} diff --git a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala index 7381bbc..2aa4196 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryCreationService.scala @@ -10,7 +10,7 @@ import org.eclipse.jgit.lib.{FileMode, Constants} trait RepositoryCreationService { - self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService => + self: AccountService with RepositoryService with LabelsService with WikiService with ActivityService with PrioritiesService => def createRepository(loginAccount: Account, owner: String, name: String, description: Option[String], isPrivate: Boolean, createReadme: Boolean) (implicit s: Session) { @@ -30,6 +30,9 @@ // Insert default labels insertDefaultLabels(owner, name) + // Insert default priorities + insertDefaultPriorities(owner, name) + // Create the actual repository val gitdir = getRepositoryDir(owner, name) JGitUtil.initRepository(gitdir) @@ -74,5 +77,13 @@ createLabel(userName, repositoryName, "wontfix", "ffffff") } + def insertDefaultPriorities(userName: String, repositoryName: String)(implicit s: Session): Unit = { + createPriority(userName, repositoryName, "highest", Some("All defects at this priority must be fixed before any public product is delivered."), "fc2929") + createPriority(userName, repositoryName, "very high", Some("Issues must be addressed before a final product is delivered."), "fc5629") + createPriority(userName, repositoryName, "high", Some("Issues should be addressed before a final product is delivered. If the issue cannot be resolved before delivery, it should be prioritized for the next release."), "fc9629") + createPriority(userName, repositoryName, "important", Some("Issues can be shipped with a final product, but should be reviewed before the next release."), "fccd29") + createPriority(userName, repositoryName, "default", Some("Default."), "acacac") + setDefaultPriority(userName, repositoryName, getPriority(userName, repositoryName, "default").map(_.priorityId)) + } } diff --git a/src/main/scala/gitbucket/core/service/RepositoryService.scala b/src/main/scala/gitbucket/core/service/RepositoryService.scala index 598031e..9426507 100644 --- a/src/main/scala/gitbucket/core/service/RepositoryService.scala +++ b/src/main/scala/gitbucket/core/service/RepositoryService.scala @@ -66,6 +66,7 @@ val issues = Issues .filter(_.byRepository(oldUserName, oldRepositoryName)).list val pullRequests = PullRequests .filter(_.byRepository(oldUserName, oldRepositoryName)).list val labels = Labels .filter(_.byRepository(oldUserName, oldRepositoryName)).list + val priorities = Priorities .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueComments = IssueComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list val issueLabels = IssueLabels .filter(_.byRepository(oldUserName, oldRepositoryName)).list val commitComments = CommitComments .filter(_.byRepository(oldUserName, oldRepositoryName)).list @@ -95,14 +96,19 @@ WebHooks .insertAll(webHooks .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) WebHookEvents.insertAll(webHookEvents .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) Milestones .insertAll(milestones .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) + Priorities .insertAll(priorities .map(_.copy(userName = newUserName, repositoryName = newRepositoryName)) :_*) IssueId .insertAll(issueId .map(_.copy(_1 = newUserName, _2 = newRepositoryName)) :_*) val newMilestones = Milestones.filter(_.byRepository(newUserName, newRepositoryName)).list + val newPriorities = Priorities.filter(_.byRepository(newUserName, newRepositoryName)).list Issues.insertAll(issues.map { x => x.copy( userName = newUserName, repositoryName = newRepositoryName, milestoneId = x.milestoneId.map { id => newMilestones.find(_.title == milestones.find(_.milestoneId == id).get.title).get.milestoneId + }, + priorityId = x.priorityId.map { id => + newPriorities.find(_.priorityName == priorities.find(_.priorityId == id).get.priorityName).get.priorityId } )} :_*) @@ -161,6 +167,7 @@ IssueComments .filter(_.byRepository(userName, repositoryName)).delete PullRequests .filter(_.byRepository(userName, repositoryName)).delete Issues .filter(_.byRepository(userName, repositoryName)).delete + Priorities .filter(_.byRepository(userName, repositoryName)).delete IssueId .filter(_.byRepository(userName, repositoryName)).delete Milestones .filter(_.byRepository(userName, repositoryName)).delete WebHooks .filter(_.byRepository(userName, repositoryName)).delete diff --git a/src/main/scala/gitbucket/core/util/StringUtil.scala b/src/main/scala/gitbucket/core/util/StringUtil.scala index d1eadf3..908fd25 100644 --- a/src/main/scala/gitbucket/core/util/StringUtil.scala +++ b/src/main/scala/gitbucket/core/util/StringUtil.scala @@ -136,6 +136,4 @@ // } // b.toString // } - - } diff --git a/src/main/twirl/gitbucket/core/dashboard/issueslist.scala.html b/src/main/twirl/gitbucket/core/dashboard/issueslist.scala.html index 7463451..ec28060 100644 --- a/src/main/twirl/gitbucket/core/dashboard/issueslist.scala.html +++ b/src/main/twirl/gitbucket/core/dashboard/issueslist.scala.html @@ -17,7 +17,7 @@ - @issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) => + @issues.map { case IssueInfo(issue, labels, milestone, priority, commentCount, commitStatus) => @issue.userName/@issue.repositoryName ・ diff --git a/src/main/twirl/gitbucket/core/issues/create.scala.html b/src/main/twirl/gitbucket/core/issues/create.scala.html index bd73e61..1522d79 100644 --- a/src/main/twirl/gitbucket/core/issues/create.scala.html +++ b/src/main/twirl/gitbucket/core/issues/create.scala.html @@ -1,5 +1,7 @@ @(collaborators: List[String], milestones: List[gitbucket.core.model.Milestone], + priorities: List[gitbucket.core.model.Priority], + defaultPriority: Option[gitbucket.core.model.Priority], labels: List[gitbucket.core.model.Label], isManageable: Boolean, content: String, @@ -29,7 +31,7 @@
- @gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), labels, isManageable, repository) + @gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), priorities, defaultPriority, labels, isManageable, repository)
diff --git a/src/main/twirl/gitbucket/core/issues/issue.scala.html b/src/main/twirl/gitbucket/core/issues/issue.scala.html index e42ffbf..e42d6b9 100644 --- a/src/main/twirl/gitbucket/core/issues/issue.scala.html +++ b/src/main/twirl/gitbucket/core/issues/issue.scala.html @@ -3,6 +3,7 @@ issueLabels: List[gitbucket.core.model.Label], collaborators: List[String], milestones: List[(gitbucket.core.model.Milestone, Int, Int)], + priorities: List[gitbucket.core.model.Priority], labels: List[gitbucket.core.model.Label], isEditable: Boolean, isManageable: Boolean, @@ -54,7 +55,7 @@ @gitbucket.core.issues.html.commentform(issue, true, isEditable, isManageable, repository)
- @gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, isManageable, repository) + @gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, priorities, None, labels, isManageable, repository)
} diff --git a/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html b/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html index 27cd255..4b0fb88 100644 --- a/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html +++ b/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html @@ -3,6 +3,8 @@ issueLabels: List[gitbucket.core.model.Label], collaborators: List[String], milestones: List[(gitbucket.core.model.Milestone, Int, Int)], + priorities: List[gitbucket.core.model.Priority], + defaultPriority: Option[gitbucket.core.model.Priority], labels: List[gitbucket.core.model.Label], isManageable: Boolean, repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) @@ -32,6 +34,40 @@ @gitbucket.core.issues.html.labellist(issueLabels)
+ +
+ Priority + @if(isManageable){ +
+ @gitbucket.core.helper.html.dropdown("Edit", right = true, filter = ("priority", "Filter Priority")) { +
  • Clear priority
  • + @priorities.map { priority => +
  • + + @gitbucket.core.helper.html.checkicon(issue.flatMap(_.priorityId).orElse(defaultPriority.map(_.priorityId)).map(id => id == priority.priorityId).getOrElse(false)) +   + @priority.priorityName + +
  • + } + } +
    + } +
    + + @issue.flatMap(_.priorityId).orElse(defaultPriority.map(_.priorityId)).map { priorityId => + @priorities.collect { case priority if(priority.priorityId == priorityId) => + @priority.priorityName + } + }.getOrElse { + No priority + } + +@if(issue.isEmpty){ + +} +
    +
    Milestone @if(isManageable){ @@ -152,6 +188,20 @@ ); }); + $('a.priority').click(function(){ + var priorityName = $(this).data('name'); + var priorityId = $(this).data('id'); + var description = $(this).attr('title'); + var color = $(this).data('color'); + var fontColor = $(this).data('font-color'); + $.post('@helpers.url(repository)/issues/@issue.issueId/priority', + { priorityId: priorityId }, + function(data){ + displayPriority(priorityName, priorityId, description, color, fontColor); + } + ); + }); + $('a.assign').click(function(){ var $this = $(this); var userName = $this.data('name'); @@ -188,6 +238,16 @@ $('input[name=milestoneId]').val(milestoneId); }); + $('a.priority').click(function(){ + var priorityName = $(this).data('name'); + var priorityId = $(this).data('id'); + var description = $(this).attr('title'); + var color = $(this).data('color'); + var fontColor = $(this).data('font-color'); + displayPriority(priorityName, priorityId, description, color, fontColor); + $('input[name=priorityId]').val(priorityId); + }); + $('a.assign').click(function(){ var $this = $(this); var userName = $this.data('name'); @@ -222,6 +282,23 @@ } } + function displayPriority(priorityName, priorityId, description, color, fontColor){ + $('a.priority i.octicon-check').removeClass('octicon-check'); + if(priorityId == ''){ + $('#label-priority').html($('').text('No priority')); + } else { + $('#label-priority').html($('').text(priorityName) + .attr('href', '@helpers.url(repository)/issues?priority=' + encodeURIComponent(priorityName) + '&state=open') + .attr('title', description) + .css({ + "background-color": color, + "color": fontColor + })); + + $('a.priority[data-id=' + priorityId + '] i').addClass('octicon-check'); + } + } + function displayAssignee($this, userName){ $('a.assign i.octicon-check').removeClass('octicon-check'); if(userName == ''){ diff --git a/src/main/twirl/gitbucket/core/issues/list.scala.html b/src/main/twirl/gitbucket/core/issues/list.scala.html index e90eb65..21d25c4 100644 --- a/src/main/twirl/gitbucket/core/issues/list.scala.html +++ b/src/main/twirl/gitbucket/core/issues/list.scala.html @@ -3,6 +3,7 @@ page: Int, collaborators: List[String], milestones: List[gitbucket.core.model.Milestone], + priorities: List[gitbucket.core.model.Priority], labels: List[gitbucket.core.model.Label], openCount: Int, closedCount: Int, @@ -38,7 +39,7 @@ } } - @gitbucket.core.issues.html.listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, labels, Some(repository), isManageable) + @gitbucket.core.issues.html.listparts(target, issues, page, openCount, closedCount, condition, collaborators, milestones, priorities, labels, Some(repository), isManageable) @if(isManageable){
    @@ -119,6 +120,9 @@ $('a.toggle-milestone').click(function(){ submitBatchEdit('@helpers.url(repository)/issues/batchedit/milestone', $(this).data('id')); }); + $('a.toggle-priority').click(function(){ + submitBatchEdit('@helpers.url(repository)/issues/batchedit/priority', $(this).data('id')); + }); }); } diff --git a/src/main/twirl/gitbucket/core/issues/listparts.scala.html b/src/main/twirl/gitbucket/core/issues/listparts.scala.html index 034aa84..9ed5013 100644 --- a/src/main/twirl/gitbucket/core/issues/listparts.scala.html +++ b/src/main/twirl/gitbucket/core/issues/listparts.scala.html @@ -6,6 +6,7 @@ condition: gitbucket.core.service.IssuesService.IssueSearchCondition, collaborators: List[String] = Nil, milestones: List[gitbucket.core.model.Milestone] = Nil, + priorities: List[gitbucket.core.model.Priority] = Nil, labels: List[gitbucket.core.model.Label] = Nil, repository: Option[gitbucket.core.service.RepositoryService.RepositoryInfo] = None, isManageable: Boolean = false)(implicit context: gitbucket.core.controller.Context) @@ -48,6 +49,22 @@ } } + @gitbucket.core.helper.html.dropdown("Priority", filter = ("priority", "Find Priority...")) { +
  • + + @gitbucket.core.helper.html.checkicon(condition.priority == Some(None)) Issues with no priority + +
  • + @priorities.map { priority => +
  • + + @gitbucket.core.helper.html.checkicon(condition.priority == Some(Some(priority.priorityName))) +    + @priority.priorityName + +
  • + } + } @gitbucket.core.helper.html.dropdown("Milestone", filter = ("milestone", "Find Milestone...")) {
  • @@ -89,6 +106,16 @@
  • + + @gitbucket.core.helper.html.checkicon(condition.sort == "priority" && condition.direction == "asc") Highest priority + +
  • +
  • + + @gitbucket.core.helper.html.checkicon(condition.sort == "priority" && condition.direction == "desc") Lowest priority + +
  • +
  • @gitbucket.core.helper.html.checkicon(condition.sort == "comments" && condition.direction == "desc") Most commented @@ -127,6 +154,14 @@
  • } } + @gitbucket.core.helper.html.dropdown("Priority", filter = ("priority", "Find Priority...")) { +
  • No priority
  • + @priorities.map { priority => +
  • +   + @priority.priorityName
  • + } + } @gitbucket.core.helper.html.dropdown("Milestone", filter = ("milestone", "Find Milestone...")) {
  • No milestone
  • @milestones.filter(_.closedDate.isEmpty).map { milestone => @@ -171,7 +206,7 @@ } - @issues.map { case IssueInfo(issue, labels, milestone, commentCount, commitStatus) => + @issues.map { case IssueInfo(issue, labels, milestone, priority, commentCount, commitStatus) => @if(isManageable){ @@ -208,6 +243,10 @@
    #@issue.issueId opened @gitbucket.core.helper.html.datetimeago(issue.registeredDate) by @helpers.user(issue.openedUserName, styleClass="username") + @priority.map(priority => priorities.filter(p => p.priorityName == priority).head).map { priority => + + @priority.priorityName + } @milestone.map { milestone => @milestone } 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..3219b4b --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/priorities/edit.scala.html @@ -0,0 +1,67 @@ +@(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..185ba84 --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/priorities/list.scala.html @@ -0,0 +1,124 @@ +@(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){ + + } + + + + + + + + + + + @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..637b11a --- /dev/null +++ b/src/main/twirl/gitbucket/core/issues/priorities/priority.scala.html @@ -0,0 +1,49 @@ +@(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.description.getOrElse("") +
    +
    +
    + @if(hasWritePermission){ + + } else if(priority.isDefault) { + + } +
    +
    +
    +
    + @counts.get(priority.priorityName).getOrElse(0) open issues +
    +
    + @if(hasWritePermission){ +
    +
    + Edit +    + Delete +
    +
    + } +
    + + diff --git a/src/main/twirl/gitbucket/core/main.scala.html b/src/main/twirl/gitbucket/core/main.scala.html index 64369ef..c252432 100644 --- a/src/main/twirl/gitbucket/core/main.scala.html +++ b/src/main/twirl/gitbucket/core/main.scala.html @@ -18,8 +18,12 @@ + + + + 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/twirl/gitbucket/core/pulls/compare.scala.html b/src/main/twirl/gitbucket/core/pulls/compare.scala.html index 714f324..ea09ab7 100644 --- a/src/main/twirl/gitbucket/core/pulls/compare.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/compare.scala.html @@ -14,6 +14,7 @@ hasOriginWritePermission: Boolean, collaborators: List[String], milestones: List[gitbucket.core.model.Milestone], + priorities: List[gitbucket.core.model.Priority], labels: List[gitbucket.core.model.Label])(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers @gitbucket.core.html.main(s"Pull requests - ${repository.owner}/${repository.name}", Some(repository)){ @@ -81,7 +82,7 @@
    - @gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map((_, 0, 0)), labels, hasOriginWritePermission, repository) + @gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map((_, 0, 0)), priorities, None, labels, hasOriginWritePermission, repository)
    diff --git a/src/main/twirl/gitbucket/core/pulls/conversation.scala.html b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html index c05d293..9ac8954 100644 --- a/src/main/twirl/gitbucket/core/pulls/conversation.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html @@ -5,6 +5,7 @@ issueLabels: List[gitbucket.core.model.Label], collaborators: List[String], milestones: List[(gitbucket.core.model.Milestone, Int, Int)], + priorities: List[gitbucket.core.model.Priority], labels: List[gitbucket.core.model.Label], isEditable: Boolean, isManageable: Boolean, @@ -45,7 +46,7 @@ }
    - @gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, labels, isManageable, repository) + @gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, priorities, None, labels, isManageable, repository)