Support multiple assignees for Issues and Pull requests (#3055)

This commit is contained in:
Naoki Takezoe
2022-05-03 22:51:54 +09:00
committed by GitHub
parent a800f305f2
commit 79dc6fc247
28 changed files with 394 additions and 175 deletions

View File

@@ -28,4 +28,26 @@
<addPrimaryKey constraintName="IDX_ISSUE_CUSTOM_FIELD_PK" tableName="ISSUE_CUSTOM_FIELD" columnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID, FIELD_ID"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_CUSTOM_FIELD_FK0" baseTableName="ISSUE_CUSTOM_FIELD" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_CUSTOM_FIELD_FK1" baseTableName="ISSUE_CUSTOM_FIELD" baseColumnNames="USER_NAME, REPOSITORY_NAME, FIELD_ID" referencedTableName="CUSTOM_FIELD" referencedColumnNames="USER_NAME, REPOSITORY_NAME, FIELD_ID"/>
<!--================================================================================================-->
<!-- ISSUE_ASSIGNEE -->
<!--================================================================================================-->
<createTable tableName="ISSUE_ASSIGNEE">
<column name="USER_NAME" type="varchar(100)" nullable="false"/>
<column name="REPOSITORY_NAME" type="varchar(100)" nullable="false"/>
<column name="ISSUE_ID" type="int" nullable="false"/>
<column name="ASSIGNEE_USER_NAME" type="varchar(100)" nullable="false"/>
</createTable>
<addPrimaryKey constraintName="IDX_ISSUE_ASSIGNEE_PK" tableName="ISSUE_ASSIGNEE" columnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNEE_USER_NAME"/>
<addForeignKeyConstraint constraintName="IDX_ISSUE_ASSIGNEE_FK0" baseTableName="ISSUE_ASSIGNEE" baseColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID" referencedTableName="ISSUE" referencedColumnNames="USER_NAME, REPOSITORY_NAME, ISSUE_ID"/>
<!--
<addForeignKeyConstraint constraintName="IDX_ISSUE_ASSIGNEE_FK1" baseTableName="ISSUE_ASSIGNEE" baseColumnNames="ASSIGNEE_USER_NAME" referencedTableName="ACCOUNT" referencedColumnNames="USER_NAME"/>
-->
<sql>
INSERT INTO ISSUE_ASSIGNEE (USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNEE_USER_NAME)
SELECT USER_NAME, REPOSITORY_NAME, ISSUE_ID, ASSIGNED_USER_NAME FROM ISSUE WHERE ASSIGNED_USER_NAME IS NOT NULL
</sql>
<dropColumn tableName="ISSUE" columnName="ASSIGNED_USER_NAME"/>
</changeSet>

View File

@@ -12,7 +12,7 @@ case class ApiIssue(
number: Int,
title: String,
user: ApiUser,
assignee: Option[ApiUser],
assignees: List[ApiUser],
labels: List[ApiLabel],
state: String,
created_at: Date,
@@ -21,7 +21,7 @@ case class ApiIssue(
milestone: Option[ApiMilestone]
)(repositoryName: RepositoryName, isPullRequest: Boolean) {
val id = 0 // dummy id
val assignees = List(assignee).flatten
val assignee = assignees.headOption
val comments_url = ApiPath(s"/api/v3/repos/${repositoryName.fullName}/issues/${number}/comments")
val html_url = ApiPath(s"/${repositoryName.fullName}/${if (isPullRequest) { "pull" } else { "issues" }}/${number}")
val pull_request = if (isPullRequest) {
@@ -43,7 +43,7 @@ object ApiIssue {
issue: Issue,
repositoryName: RepositoryName,
user: ApiUser,
assignee: Option[ApiUser],
assignees: List[ApiUser],
labels: List[ApiLabel],
milestone: Option[ApiMilestone]
): ApiIssue =
@@ -51,7 +51,7 @@ object ApiIssue {
number = issue.issueId,
title = issue.title,
user = user,
assignee = assignee,
assignees = assignees,
labels = labels,
milestone = milestone,
state = if (issue.closed) { "closed" } else { "open" },

View File

@@ -21,10 +21,11 @@ case class ApiPullRequest(
body: String,
user: ApiUser,
labels: List[ApiLabel],
assignee: Option[ApiUser],
assignees: List[ApiUser],
draft: Option[Boolean]
) {
val id = 0 // dummy id
val assignee = assignees.headOption
val html_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}")
//val diff_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.diff")
//val patch_url = ApiPath(s"${base.repo.html_url.path}/pull/${number}.patch")
@@ -45,7 +46,7 @@ object ApiPullRequest {
baseRepo: ApiRepository,
user: ApiUser,
labels: List[ApiLabel],
assignee: Option[ApiUser],
assignees: List[ApiUser],
mergedComment: Option[(IssueComment, Account)]
): ApiPullRequest =
ApiPullRequest(
@@ -63,7 +64,7 @@ object ApiPullRequest {
body = issue.content.getOrElse(""),
user = user,
labels = labels,
assignee = assignee,
assignees = assignees,
draft = Some(pullRequest.isDraft)
)

View File

@@ -53,7 +53,7 @@ trait IssuesControllerBase extends ControllerBase {
case class IssueCreateForm(
title: String,
content: Option[String],
assignedUserName: Option[String],
assigneeUserNames: Option[String],
milestoneId: Option[Int],
priorityId: Option[Int],
labelNames: Option[String]
@@ -64,7 +64,7 @@ trait IssuesControllerBase extends ControllerBase {
val issueCreateForm = mapping(
"title" -> trim(label("Title", text(required))),
"content" -> trim(optional(text())),
"assignedUserName" -> trim(optional(text())),
"assigneeUserNames" -> trim(optional(text())),
"milestoneId" -> trim(optional(number())),
"priorityId" -> trim(optional(number())),
"labelNames" -> trim(optional(text()))
@@ -107,6 +107,7 @@ trait IssuesControllerBase extends ControllerBase {
issue,
getComments(repository.owner, repository.name, issueId.toInt),
getIssueLabels(repository.owner, repository.name, issueId.toInt),
getIssueAssignees(repository.owner, repository.name, issueId.toInt),
getAssignableUserNames(repository.owner, repository.name),
getMilestonesWithIssueCount(repository.owner, repository.name),
getPriorities(repository.owner, repository.name),
@@ -145,7 +146,7 @@ trait IssuesControllerBase extends ControllerBase {
repository,
form.title,
form.content,
form.assignedUserName,
form.assigneeUserNames.toSeq.flatMap(_.split(",")),
form.milestoneId,
form.priorityId,
form.labelNames.toSeq.flatMap(_.split(",")),
@@ -356,15 +357,16 @@ trait IssuesControllerBase extends ControllerBase {
html.labellist(getIssueLabels(repository.owner, repository.name, issueId))
})
ajaxPost("/:owner/:repository/issues/:id/assign")(writableUsersOnly { repository =>
updateAssignedUserName(
repository.owner,
repository.name,
params("id").toInt,
assignedUserName("assignedUserName"),
true
)
Ok("updated")
ajaxPost("/:owner/:repository/issues/:id/assignee/new")(writableUsersOnly { repository =>
val issueId = params("id").toInt
registerIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
Ok()
})
ajaxPost("/:owner/:repository/issues/:id/assignee/delete")(writableUsersOnly { repository =>
val issueId = params("id").toInt
deleteIssueAssignee(repository.owner, repository.name, issueId, params("assigneeUserName"), true)
Ok()
})
ajaxPost("/:owner/:repository/issues/:id/milestone")(writableUsersOnly { repository =>
@@ -455,7 +457,13 @@ trait IssuesControllerBase extends ControllerBase {
post("/:owner/:repository/issues/batchedit/assign")(writableUsersOnly { repository =>
val value = assignedUserName("value")
executeBatch(repository) {
updateAssignedUserName(repository.owner, repository.name, _, value, true)
//updateAssignedUserName(repository.owner, repository.name, _, value, true)
value match {
case Some(assignedUserName) =>
registerIssueAssignee(repository.owner, repository.name, _, assignedUserName, true)
case None =>
deleteAllIssueAssignees(repository.owner, repository.name, _, true)
}
}
if (params("uri").nonEmpty) {
redirect(params("uri"))

View File

@@ -44,7 +44,7 @@ trait LabelsControllerBase extends ControllerBase {
get("/:owner/:repository/issues/labels")(referrersOnly { repository =>
html.list(
getLabels(repository.owner, repository.name),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
)
@@ -59,7 +59,7 @@ trait LabelsControllerBase extends ControllerBase {
html.label(
getLabel(repository.owner, repository.name, labelId).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
)
@@ -76,7 +76,7 @@ trait LabelsControllerBase extends ControllerBase {
html.label(
getLabel(repository.owner, repository.name, params("labelId").toInt).get,
// TODO futility
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition(), Map.empty),
countIssueGroupByLabels(repository.owner, repository.name, IssuesService.IssueSearchCondition()),
repository,
hasDeveloperRole(repository.owner, repository.name, context.loginAccount)
)

View File

@@ -92,7 +92,7 @@ trait PullRequestsControllerBase extends ControllerBase {
commitIdFrom: String,
commitIdTo: String,
isDraft: Boolean,
assignedUserName: Option[String],
assignedUserNames: Option[String],
milestoneId: Option[Int],
priorityId: Option[Int],
labelNames: Option[String]
@@ -131,6 +131,7 @@ trait PullRequestsControllerBase extends ControllerBase {
getPullRequestComments(repository.owner, repository.name, issue.issueId, commits.flatten),
diffs.size,
getIssueLabels(repository.owner, repository.name, issueId),
getIssueAssignees(repository.owner, repository.name, issueId),
getAssignableUserNames(repository.owner, repository.name),
getMilestonesWithIssueCount(repository.owner, repository.name),
getPriorities(repository.owner, repository.name),
@@ -571,7 +572,6 @@ trait PullRequestsControllerBase extends ControllerBase {
loginUser = loginAccount.userName,
title = form.title,
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
@@ -591,8 +591,14 @@ trait PullRequestsControllerBase extends ControllerBase {
settings = context.settings
)
// insert labels
if (manageable) {
// insert assignees
form.assignedUserNames.foreach { value =>
value.split(",").foreach { userName =>
registerIssueAssignee(repository.owner, repository.name, issueId, userName)
}
}
// insert labels
form.labelNames.foreach { value =>
val labels = getLabels(repository.owner, repository.name)
value.split(",").foreach { labelName =>

View File

@@ -679,7 +679,6 @@ trait RepositoryViewerControllerBase extends ControllerBase {
loginUser = loginAccount.userName,
title = requestBranch,
content = commitMessage,
assignedUserName = None,
milestoneId = None,
priorityId = None,
isPullRequest = true

View File

@@ -29,9 +29,9 @@ trait ApiIssueControllerBase extends ControllerBase {
val page = IssueSearchCondition.page(request)
// TODO: more api spec condition
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
//val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Option[Account])] =
val issues: List[(Issue, Account, List[Account])] =
searchIssueByApi(
condition = condition,
offset = (page - 1) * PullRequestLimit,
@@ -40,12 +40,12 @@ trait ApiIssueControllerBase extends ControllerBase {
)
JsonFormat(issues.map {
case (issue, issueUser, assignedUser) =>
case (issue, issueUser, assigneeUsers) =>
ApiIssue(
issue = issue,
repositoryName = RepositoryName(repository),
user = ApiUser(issueUser),
assignee = assignedUser.map(ApiUser(_)),
assignees = assigneeUsers.map(ApiUser(_)),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
@@ -61,7 +61,8 @@ trait ApiIssueControllerBase extends ControllerBase {
(for {
issueId <- params("id").toIntOpt
issue <- getIssue(repository.owner, repository.name, issueId.toString)
users = getAccountsByUserNames(Set(issue.openedUserName) ++ issue.assignedUserName, Set())
assigneeUsers = getIssueAssignees(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(Set(issue.openedUserName) ++ assigneeUsers.map(_.assigneeUserName), Set())
openedUser <- users.get(issue.openedUserName)
} yield {
JsonFormat(
@@ -69,7 +70,7 @@ trait ApiIssueControllerBase extends ControllerBase {
issue,
RepositoryName(repository),
ApiUser(openedUser),
issue.assignedUserName.flatMap(users.get(_)).map(ApiUser(_)),
assigneeUsers.flatMap(x => users.get(x.assigneeUserName)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId).map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) }
)
@@ -92,7 +93,7 @@ trait ApiIssueControllerBase extends ControllerBase {
repository,
data.title,
data.body,
data.assignees.headOption,
data.assignees,
milestone.map(_.milestoneId),
None,
data.labels,
@@ -103,7 +104,9 @@ trait ApiIssueControllerBase extends ControllerBase {
issue,
RepositoryName(repository),
ApiUser(loginAccount),
issue.assignedUserName.flatMap(getAccountByUserName(_)).map(ApiUser(_)),
getIssueAssignees(repository.owner, repository.name, issue.issueId)
.flatMap(x => getAccountByUserName(x.assigneeUserName, false))
.map(ApiUser.apply),
getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
issue.milestoneId.flatMap { getApiMilestone(repository, _) }

View File

@@ -40,7 +40,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
val condition = IssueSearchCondition(request)
val baseOwner = getAccountByUserName(repository.owner).get
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] =
val issues: List[(Issue, Account, Int, PullRequest, Repository, Account, List[Account])] =
searchPullRequestByApi(
condition = condition,
offset = (page - 1) * PullRequestLimit,
@@ -49,7 +49,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
)
JsonFormat(issues.map {
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignee) =>
case (issue, issueUser, commentCount, pullRequest, headRepo, headOwner, assignees) =>
ApiPullRequest(
issue = issue,
pullRequest = pullRequest,
@@ -58,7 +58,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
user = ApiUser(issueUser),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
assignee = assignee.map(ApiUser.apply),
assignees = assignees.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
})
@@ -99,7 +99,6 @@ trait ApiPullRequestControllerBase extends ControllerBase {
loginUser = context.loginAccount.get.userName,
title = createPullReq.title,
content = createPullReq.body,
assignedUserName = None,
milestoneId = None,
priorityId = None,
isPullRequest = true
@@ -319,8 +318,8 @@ trait ApiPullRequestControllerBase extends ControllerBase {
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName =>
getAccountByUserName(userName, false)
assignees = getIssueAssignees(repository.owner, repository.name, issueId).flatMap { assignedUser =>
getAccountByUserName(assignedUser.assigneeUserName, false)
}
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
} yield {
@@ -332,7 +331,7 @@ trait ApiPullRequestControllerBase extends ControllerBase {
user = ApiUser(issueUser),
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
assignee = assignee.map(ApiUser.apply),
assignees = assignees.map(ApiUser.apply),
mergedComment = getMergedComment(repository.owner, repository.name, issue.issueId)
)
}

View File

@@ -27,7 +27,6 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
with MilestoneTemplate
with PriorityTemplate {
val openedUserName = column[String]("OPENED_USER_NAME")
val assignedUserName = column[String]("ASSIGNED_USER_NAME")
val title = column[String]("TITLE")
val content = column[String]("CONTENT")
val closed = column[Boolean]("CLOSED")
@@ -42,7 +41,6 @@ trait IssueComponent extends TemplateComponent { self: Profile =>
openedUserName,
milestoneId.?,
priorityId.?,
assignedUserName.?,
title,
content.?,
closed,
@@ -62,7 +60,6 @@ case class Issue(
openedUserName: String,
milestoneId: Option[Int],
priorityId: Option[Int],
assignedUserName: Option[String],
title: String,
content: Option[String],
closed: Boolean,

View File

@@ -0,0 +1,26 @@
package gitbucket.core.model
trait IssueAssigneeComponent extends TemplateComponent { self: Profile =>
import profile.api._
import self._
lazy val IssueAssignees = TableQuery[IssueAssignees]
class IssueAssignees(tag: Tag) extends Table[IssueAssignee](tag, "ISSUE_ASSIGNEE") with IssueTemplate {
val assigneeUserName = column[String]("ASSIGNEE_USER_NAME")
def * =
(userName, repositoryName, issueId, assigneeUserName)
.<>(IssueAssignee.tupled, IssueAssignee.unapply)
def byPrimaryKey(owner: String, repository: String, issueId: Int, assigneeUserName: String) = {
byIssue(owner, repository, issueId) && this.assigneeUserName === assigneeUserName.bind
}
}
}
case class IssueAssignee(
userName: String,
repositoryName: String,
issueId: Int,
assigneeUserName: String
)

View File

@@ -74,5 +74,6 @@ trait CoreProfile
with AccountPreferenceComponent
with CustomFieldComponent
with IssueCustomFieldComponent
with IssueAssigneeComponent
object Profile extends CoreProfile

View File

@@ -16,7 +16,7 @@ trait IssueCreationService {
repository: RepositoryInfo,
title: String,
body: Option[String],
assignee: Option[String],
assignees: Seq[String],
milestoneId: Option[Int],
priorityId: Option[Int],
labelNames: Seq[String],
@@ -35,16 +35,19 @@ trait IssueCreationService {
userName,
title,
body,
if (manageable) assignee else None,
if (manageable) milestoneId else None,
if (manageable) priorityId else None
)
val issue: Issue = getIssue(owner, name, issueId.toString).get
// insert labels
if (manageable) {
// insert assignees
assignees.foreach { assignee =>
registerIssueAssignee(owner, name, issueId, assignee)
}
// insert labels
val labels = getLabels(owner, name)
labelNames.map { labelName =>
labelNames.foreach { labelName =>
labels.find(_.labelName == labelName).map { label =>
registerIssueLabel(owner, name, issueId, label.labelId)
}

View File

@@ -5,7 +5,17 @@ import gitbucket.core.util.StringUtil._
import gitbucket.core.util.Implicits._
import gitbucket.core.util.SyntaxSugars._
import gitbucket.core.controller.Context
import gitbucket.core.model.{Account, Issue, IssueComment, IssueLabel, Label, PullRequest, Repository, Role}
import gitbucket.core.model.{
Account,
Issue,
IssueAssignee,
IssueComment,
IssueLabel,
Label,
PullRequest,
Repository,
Role
}
import gitbucket.core.model.Profile._
import gitbucket.core.model.Profile.profile._
import gitbucket.core.model.Profile.profile.blockingApi._
@@ -118,8 +128,7 @@ trait IssuesService {
def countIssueGroupByLabels(
owner: String,
repository: String,
condition: IssueSearchCondition,
filterUser: Map[String, String]
condition: IssueSearchCondition
)(implicit s: Session): Map[String, Int] = {
searchIssueQuery(Seq(owner -> repository), condition.copy(labels = Set.empty), IssueSearchOption.Issues)
@@ -244,20 +253,31 @@ trait IssuesService {
}
/** for api
* @return (issue, issueUser, assignedUser)
* @return (issue, issueUser, Seq(assigneeUsers))
*/
def searchIssueByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
implicit s: Session
): List[(Issue, Account, Option[Account])] = {
): List[(Issue, Account, List[Account])] = {
// get issues and comment count and labels
searchIssueQueryBase(condition, IssueSearchOption.Issues, offset, limit, repos)
.join(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 => t3.userName === t1.openedUserName }
.joinLeft(IssueAssignees)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
.joinLeft(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 => t4.userName === t1.assignedUserName }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 => (t1, t3, t4) }
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t4.map(_.assigneeUserName) }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => (t1, t3, t5) }
.list
.groupBy {
case (issue, account, assignedUsers) =>
(issue, account)
}
.map {
case (_, values) =>
(values.head._1, values.head._2, values.flatMap(_._3))
}
.toList
}
/** for api
@@ -265,7 +285,7 @@ trait IssuesService {
*/
def searchPullRequestByApi(condition: IssueSearchCondition, offset: Int, limit: Int, repos: (String, String)*)(
implicit s: Session
): List[(Issue, Account, Int, PullRequest, Repository, Account, Option[Account])] = {
): List[(Issue, Account, Int, PullRequest, Repository, Account, List[Account])] = {
// get issues and comment count and labels
searchIssueQueryBase(condition, IssueSearchOption.PullRequests, offset, limit, repos)
.join(PullRequests)
@@ -276,11 +296,30 @@ trait IssuesService {
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 => t5.userName === t1.openedUserName }
.join(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 => t6.userName === t4.userName }
.joinLeft(IssueAssignees)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.byIssue(t1.userName, t1.repositoryName, t1.issueId) }
.joinLeft(Accounts)
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => t7.userName === t1.assignedUserName }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 => (t1, t5, t2.commentCount, t3, t4, t6, t7) }
.on { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => t8.userName === t7.map(_.assigneeUserName) }
.sortBy { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => i asc }
.map { case t1 ~ t2 ~ i ~ t3 ~ t4 ~ t5 ~ t6 ~ t7 ~ t8 => (t1, t5, t2.commentCount, t3, t4, t6, t8) }
.list
.groupBy {
case (issue, openedUser, commentCount, pullRequest, repository, account, assignedUser) =>
(issue, openedUser, commentCount, pullRequest, repository, account)
}
.map {
case (_, values) =>
(
values.head._1,
values.head._2,
values.head._3,
values.head._4,
values.head._5,
values.head._6,
values.flatMap(_._7)
)
}
.toList
}
private def searchIssueQueryBase(
@@ -347,7 +386,7 @@ trait IssuesService {
case _ => t1.closed === true || t1.closed === false
}).&&(t1.milestoneId.? isEmpty, condition.milestone == Some(None))
.&&(t1.priorityId.? isEmpty, condition.priority == Some(None))
.&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None))
//.&&(t1.assignedUserName.? isEmpty, condition.assigned == Some(None))
.&&(t1.openedUserName === condition.author.get.bind, condition.author.isDefined) &&
(searchOption match {
case IssueSearchOption.Issues => t1.pullRequest === false
@@ -371,7 +410,13 @@ trait IssuesService {
condition.priority.flatten.isDefined
)
// Assignee filter
.&&(t1.assignedUserName === condition.assigned.get.get.bind, condition.assigned.flatten.isDefined)
.&&(
IssueAssignees filter { a =>
a.byIssue(t1.userName, t1.repositoryName, t1.issueId) &&
a.assigneeUserName === condition.assigned.get.get.bind
} exists,
condition.assigned.flatten.isDefined
)
// Label filter
.&&(
IssueLabels filter { t2 =>
@@ -396,7 +441,9 @@ trait IssuesService {
.&&(t1.userName inSetBind condition.groups, condition.groups.nonEmpty)
// Mentioned filter
.&&(
(t1.openedUserName === condition.mentioned.get.bind) || t1.assignedUserName === condition.mentioned.get.bind ||
(t1.openedUserName === condition.mentioned.get.bind) || (IssueAssignees filter { t1 =>
t1.byIssue(t1.userName, t1.repositoryName, t1.issueId) && t1.assigneeUserName === condition.mentioned.get.bind
} exists) ||
(IssueComments filter { t2 =>
(t2.byIssue(t1.userName, t1.repositoryName, t1.issueId)) && (t2.commentedUserName === condition.mentioned.get.bind)
} exists),
@@ -410,7 +457,6 @@ trait IssuesService {
loginUser: String,
title: String,
content: Option[String],
assignedUserName: Option[String],
milestoneId: Option[Int],
priorityId: Option[Int],
isPullRequest: Boolean = false
@@ -427,7 +473,6 @@ trait IssuesService {
loginUser,
milestoneId,
priorityId,
assignedUserName,
title,
content,
false,
@@ -542,35 +587,91 @@ trait IssuesService {
.update(true)
}
def updateAssignedUserName(
def getIssueAssignees(owner: String, repository: String, issueId: Int)(
implicit s: Session
): List[IssueAssignee] = {
IssueAssignees.filter(_.byIssue(owner, repository, issueId)).sortBy(_.assigneeUserName).list
}
def registerIssueAssignee(
owner: String,
repository: String,
issueId: Int,
assignedUserName: Option[String],
assigneeUserName: String,
insertComment: Boolean = false
)(implicit context: Context, s: Session): Int = {
val oldAssigned = getIssue(owner, repository, s"${issueId}").get.assignedUserName
val assigned = assignedUserName
)(
implicit context: Context,
s: Session
): Int = {
val assigner = context.loginAccount.map(_.userName)
if (insertComment) {
IssueComments insert IssueComment(
userName = owner,
repositoryName = repository,
issueId = issueId,
action = "assign",
action = "add_assignee",
commentedUserName = assigner.getOrElse("Unknown user"),
content = s"""${oldAssigned.getOrElse("Not assigned")}:${assigned.getOrElse("Not assigned")}""",
content = assigneeUserName,
registeredDate = currentDate,
updatedDate = currentDate
)
}
for (issue <- getIssue(owner, repository, issueId.toString); repo <- getRepository(owner, repository)) {
PluginRegistry().getIssueHooks.foreach(_.assigned(issue, repo, assigner, assigned, oldAssigned))
PluginRegistry().getIssueHooks.foreach(_.assigned(issue, repo, assigner, Some(assigneeUserName), None))
}
Issues
.filter(_.byPrimaryKey(owner, repository, issueId))
.map(t => (t.assignedUserName ?, t.updatedDate))
.update(assignedUserName, currentDate)
IssueAssignees insert IssueAssignee(owner, repository, issueId, assigneeUserName)
}
def deleteIssueAssignee(
owner: String,
repository: String,
issueId: Int,
assigneeUserName: String,
insertComment: Boolean = false
)(
implicit context: Context,
s: Session
): Int = {
val assigner = context.loginAccount.map(_.userName)
if (insertComment) {
IssueComments insert IssueComment(
userName = owner,
repositoryName = repository,
issueId = issueId,
action = "delete_assignee",
commentedUserName = assigner.getOrElse("Unknown user"),
content = assigneeUserName,
registeredDate = currentDate,
updatedDate = currentDate
)
}
// TODO Notify plugins of unassignment as doing in registerIssueAssignee()?
IssueAssignees filter (_.byPrimaryKey(owner, repository, issueId, assigneeUserName)) delete
}
def deleteAllIssueAssignees(owner: String, repository: String, issueId: Int, insertComment: Boolean = false)(
implicit context: Context,
s: Session
): Int = {
val assigner = context.loginAccount.map(_.userName)
if (insertComment) {
IssueComments insert IssueComment(
userName = owner,
repositoryName = repository,
issueId = issueId,
action = "delete_assign",
commentedUserName = assigner.getOrElse("Unknown user"),
content = "All assignees",
registeredDate = currentDate,
updatedDate = currentDate
)
}
// TODO Notify plugins of unassignment as doing in registerIssueAssignee()?
IssueAssignees filter (_.byIssue(owner, repository, issueId)) delete
}
def updateMilestoneId(

View File

@@ -379,8 +379,12 @@ trait WebHookPullRequestService extends WebHookService {
settings: SystemSettings
)(implicit s: Session, context: JsonFormat.Context): Unit = {
callWebHookOf(repository.owner, repository.name, WebHook.Issues, settings) {
val assigneeUsers = getIssueAssignees(repository.owner, repository.name, issue.issueId)
val users =
getAccountsByUserNames(Set(repository.owner, issue.openedUserName) ++ issue.assignedUserName, Set(sender))
getAccountsByUserNames(
Set(repository.owner, issue.openedUserName) ++ assigneeUsers.map(_.assigneeUserName),
Set(sender)
)
for {
repoOwner <- users.get(repository.owner)
issueUser <- users.get(issue.openedUserName)
@@ -393,7 +397,7 @@ trait WebHookPullRequestService extends WebHookService {
issue,
RepositoryName(repository),
ApiUser(issueUser),
issue.assignedUserName.flatMap(users.get(_)).map(ApiUser(_)),
assigneeUsers.flatMap(x => users.get(x.assigneeUserName)).map(ApiUser(_)),
getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository))),
getApiMilestone(repository, issue.milestoneId getOrElse (0))
@@ -415,16 +419,15 @@ trait WebHookPullRequestService extends WebHookService {
callWebHookOf(repository.owner, repository.name, WebHook.PullRequest, settings) {
for {
(issue, pullRequest) <- getPullRequest(repository.owner, repository.name, issueId)
assignees = getIssueAssignees(repository.owner, repository.name, issueId)
users = getAccountsByUserNames(
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName),
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName) ++ assignees.map(_.assigneeUserName),
Set(sender)
)
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName =>
getAccountByUserName(userName, false)
}
assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
.map(ApiLabel(_, RepositoryName(repository)))
@@ -433,7 +436,7 @@ trait WebHookPullRequestService extends WebHookService {
action = action,
issue = issue,
issueUser = issueUser,
assignee = assignee,
assignees = assigneeUsers,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -481,8 +484,8 @@ trait WebHookPullRequestService extends WebHookService {
requestRepository.name,
requestBranch
)
assignee = issue.assignedUserName.flatMap { userName =>
getAccountByUserName(userName, false)
assignees = getIssueAssignees(requestRepository.owner, requestRepository.name, issue.issueId).flatMap { x =>
getAccountByUserName(x.assigneeUserName, false)
}
baseRepo <- getRepository(pullRequest.userName, pullRequest.repositoryName)
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
@@ -492,7 +495,7 @@ trait WebHookPullRequestService extends WebHookService {
action = action,
issue = issue,
issueUser = issueUser,
assignee = assignee,
assignees = assignees,
pullRequest = pullRequest,
headRepository = requestRepository,
headOwner = headOwner,
@@ -522,15 +525,17 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
)(implicit s: Session, c: JsonFormat.Context): Unit = {
import WebHookService._
callWebHookOf(repository.owner, repository.name, WebHook.PullRequestReviewComment, settings) {
val assignees = getIssueAssignees(repository.owner, pullRequest.requestUserName, issue.issueId)
val users =
getAccountsByUserNames(Set(repository.owner, pullRequest.requestUserName, issue.openedUserName), Set(sender))
getAccountsByUserNames(
Set(repository.owner, pullRequest.requestUserName, issue.openedUserName) ++ assignees.map(_.assigneeUserName),
Set(sender)
)
for {
baseOwner <- users.get(repository.owner)
headOwner <- users.get(pullRequest.requestUserName)
issueUser <- users.get(issue.openedUserName)
assignee = issue.assignedUserName.flatMap { userName =>
getAccountByUserName(userName, false)
}
assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
headRepo <- getRepository(pullRequest.requestUserName, pullRequest.requestRepositoryName)
labels = getIssueLabels(pullRequest.userName, pullRequest.repositoryName, issue.issueId)
.map(ApiLabel(_, RepositoryName(pullRequest.userName, pullRequest.repositoryName)))
@@ -540,7 +545,7 @@ trait WebHookPullRequestReviewCommentService extends WebHookService {
comment = comment,
issue = issue,
issueUser = issueUser,
assignee = assignee,
assignees = assigneeUsers,
pullRequest = pullRequest,
headRepository = headRepo,
headOwner = headOwner,
@@ -569,14 +574,17 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
callWebHookOf(repository.owner, repository.name, WebHook.IssueComment, settings) {
for {
issueComment <- getComment(repository.owner, repository.name, issueCommentId.toString())
assignees = getIssueAssignees(repository.owner, repository.name, issue.issueId)
users = getAccountsByUserNames(
Set(issue.openedUserName, repository.owner, issueComment.commentedUserName) ++ issue.assignedUserName,
Set(issue.openedUserName, repository.owner, issueComment.commentedUserName) ++ assignees.map(
_.assigneeUserName
),
Set(sender)
)
issueUser <- users.get(issue.openedUserName)
repoOwner <- users.get(repository.owner)
commenter <- users.get(issueComment.commentedUserName)
assignedUser = issue.assignedUserName.flatMap(users.get(_))
assigneeUsers = assignees.flatMap(x => users.get(x.assigneeUserName))
labels = getIssueLabels(repository.owner, repository.name, issue.issueId)
milestone = getApiMilestone(repository, issue.milestoneId getOrElse (0))
} yield {
@@ -587,7 +595,7 @@ trait WebHookIssueCommentService extends WebHookPullRequestService {
commentUser = commenter,
repository = repository,
repositoryUser = repoOwner,
assignedUser = assignedUser,
assignees = assigneeUsers,
sender = sender,
labels = labels,
milestone = milestone
@@ -710,7 +718,7 @@ object WebHookService {
action: String,
issue: Issue,
issueUser: Account,
assignee: Option[Account],
assignees: List[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -731,7 +739,7 @@ object WebHookService {
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
labels = labels,
assignee = assignee.map(ApiUser.apply),
assignees = assignees.map(ApiUser.apply),
mergedComment = mergedComment
)
@@ -762,7 +770,7 @@ object WebHookService {
commentUser: Account,
repository: RepositoryInfo,
repositoryUser: Account,
assignedUser: Option[Account],
assignees: List[Account],
sender: Account,
labels: List[Label],
milestone: Option[ApiMilestone]
@@ -774,7 +782,7 @@ object WebHookService {
issue,
RepositoryName(repository),
ApiUser(issueUser),
assignedUser.map(ApiUser(_)),
assignees.map(ApiUser(_)),
labels.map(ApiLabel(_, RepositoryName(repository))),
milestone
),
@@ -799,7 +807,7 @@ object WebHookService {
comment: CommitComment,
issue: Issue,
issueUser: Account,
assignee: Option[Account],
assignees: List[Account],
pullRequest: PullRequest,
headRepository: RepositoryInfo,
headOwner: Account,
@@ -828,7 +836,7 @@ object WebHookService {
baseRepo = baseRepoPayload,
user = ApiUser(issueUser),
labels = labels,
assignee = assignee.map(ApiUser.apply),
assignees = assignees.map(ApiUser.apply),
mergedComment = mergedComment
),
repository = baseRepoPayload,

View File

@@ -33,9 +33,11 @@
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right muted">
@*
@issue.assignedUserName.map { userName =>
@helpers.avatar(userName, 20, tooltip = true)
}
*@
@if(commentCount > 0){
<a href="@context.path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<i class="octicon octicon-comment active"></i> @commentCount

View File

@@ -200,7 +200,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-tag"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
add the <code>@comment.content</code> label
added the <code>@comment.content</code> label
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
@@ -222,7 +222,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-flame"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
change priority from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
changed priority from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
@@ -233,18 +233,40 @@
<span class="discussion-item-icon"><i class="octicon octicon-milestone"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
change milestone from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
changed milestone from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
case "assign" => {
case "assign" => { @* for backward compatibility *@
<div class="discussion-item discussion-item-assign">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
change assignee from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
changed assignee from <code>@comment.content.split(":")(0)</code> to <code>@comment.content.split(":")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
case "add_assignee" => {
<div class="discussion-item discussion-item-assign">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
assigned <code>@comment.content</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
}
case "delete_assignee" => {
<div class="discussion-item discussion-item-assign">
<div class="discussion-item-header">
<span class="discussion-item-icon"><i class="octicon octicon-person"></i></span>
@helpers.avatarLink(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
unassigned <code>@comment.content</code></code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
@@ -255,7 +277,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-pencil"></i></span>
@helpers.avatar(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
change title from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
changed title from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
@@ -266,7 +288,7 @@
<span class="discussion-item-icon"><i class="octicon octicon-pencil"></i></span>
@helpers.avatar(comment.commentedUserName, 16)
@helpers.user(comment.commentedUserName, styleClass="username strong")
change base branch from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
changed base branch from <code>@convertLineSeparator(comment.content, "LF").split("\n")(0)</code> to <code>@convertLineSeparator(comment.content, "LF").split("\n")(1)</code>
@gitbucket.core.helper.html.datetimeago(comment.registeredDate)
</div>
</div>
@@ -328,9 +350,9 @@
$(function(){
@if(issue.isDefined){
$('.issue-comment-box i.octicon-pencil').click(function(){
var id = $(this).closest('a').data('comment-id');
var url = '@helpers.url(repository)/issue_comments/_data/' + id;
var $content = $('#commentContent-' + id);
let id = $(this).closest('a').data('comment-id');
let url = '@helpers.url(repository)/issue_comments/_data/' + id;
let $content = $('#commentContent-' + id);
if(!id){
id = $(this).closest('a').data('issue-id');
@@ -345,7 +367,7 @@ $(function(){
});
$('.issue-comment-box i.octicon-x').click(function(){
if(confirm('Are you sure you want to delete this?')) {
var id = $(this).closest('a').data('comment-id');
const id = $(this).closest('a').data('comment-id');
$.post('@helpers.url(repository)/issue_comments/delete/' + id, function(data){
if(data > 0) {
$('#comment-' + id).remove();
@@ -356,9 +378,9 @@ $(function(){
});
}
$(document).on('click', '.commit-comment-box i.octicon-pencil', function(){
var id = $(this).closest('a').data('comment-id');
var url = '@helpers.url(repository)/commit_comments/_data/' + id;
var $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
const id = $(this).closest('a').data('comment-id');
const url = '@helpers.url(repository)/commit_comments/_data/' + id;
const $content = $('.commit-commentContent-' + id, $(this).closest('.commit-comment-box'));
$.get(url, { dataType : 'html' }, function(data){
$content.empty().html(data);
@@ -369,14 +391,14 @@ $(function(){
$(document).on('click', '.commit-comment-box i.octicon-x', function(){
if(confirm('Are you sure you want to delete this?')) {
var id = $(this).closest('a').data('comment-id');
const id = $(this).closest('a').data('comment-id');
$.post('@helpers.url(repository)/commit_comments/delete/' + id,
function(data){
if(data > 0) {
var comment = $('.commit-comment-' + id);
const comment = $('.commit-comment-' + id);
// diff view
var tr = comment.closest('.not-diff');
const tr = comment.closest('.not-diff');
if(tr.length > 0){
if(tr.prev('.not-diff').length == 0){
tr.next('.not-diff:has(.reply-comment)').remove();
@@ -385,7 +407,7 @@ $(function(){
}
// comment list view
var panel = comment.closest('div.panel:has(.commit-comment-box)');
const panel = comment.closest('div.panel:has(.commit-comment-box)');
if(panel.length > 0){
comment.parent('.commit-comment-box').remove();
if(panel.has('.commit-comment-box').length == 0){
@@ -401,7 +423,7 @@ $(function(){
});
$('div[class*=commit-commentContent-]').on('click', ':checkbox', function(ev){
var $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'),
const $commentContent = $(ev.target).parents('div[class*=commit-commentContent-]'),
commentId = $commentContent.attr('class').match(/commit-commentContent-.+/)[0].replace(/commit-commentContent-/, ''),
checkboxes = $commentContent.find(':checkbox');
$.get('@helpers.url(repository)/commit_comments/_data/' + commentId, { dataType : 'html' },
@@ -421,9 +443,9 @@ $(function(){
);
});
@if(issue.isDefined){
@if(issue.isDefined){
$('#issueContent').on('click', ':checkbox', function(ev){
var checkboxes = $('#issueContent :checkbox');
const checkboxes = $('#issueContent :checkbox');
$.get('@helpers.url(repository)/issues/_data/@issue.get.issueId', { dataType : 'html' },
function(responseContent){
$.ajax({
@@ -439,7 +461,7 @@ $(function(){
});
$('div[id^=commentContent-]').on('click', ':checkbox', function(ev){
var $commentContent = $(ev.target).parents('div[id^=commentContent-]'),
const $commentContent = $(ev.target).parents('div[id^=commentContent-]'),
commentId = $commentContent.attr('id').replace(/commentContent-/, ''),
checkboxes = $commentContent.find(':checkbox');
$.get('@helpers.url(repository)/issue_comments/_data/' + commentId, { dataType : 'html' },
@@ -455,9 +477,7 @@ $(function(){
}
);
});
}
}
});
</script>
}

View File

@@ -36,6 +36,7 @@
issue = None,
comments = Nil,
issueLabels = Nil,
issueAssignees = Nil,
collaborators = collaborators,
milestones = milestones.map(x => (x, 0, 0)),
priorities= priorities,

View File

@@ -1,6 +1,7 @@
@(issue: gitbucket.core.model.Issue,
comments: List[gitbucket.core.model.IssueComment],
issueLabels: List[gitbucket.core.model.Label],
issueAssignees: List[gitbucket.core.model.IssueAssignee],
collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
priorities: List[gitbucket.core.model.Priority],
@@ -61,6 +62,7 @@
issue = Some(issue),
comments = comments,
issueLabels = issueLabels,
issueAssignees = issueAssignees,
collaborators = collaborators,
milestones = milestones,
priorities = priorities,

View File

@@ -2,6 +2,7 @@
@(issue: Option[gitbucket.core.model.Issue],
comments: List[gitbucket.core.model.Comment],
issueLabels: List[gitbucket.core.model.Label],
issueAssignees: List[gitbucket.core.model.IssueAssignee],
collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
priorities: List[gitbucket.core.model.Priority],
@@ -127,11 +128,10 @@
@if(isManageable){
<div class="pull-right">
@gitbucket.core.helper.html.dropdown("Edit", right = true, filter = ("assignee", "Filter Assignee")) {
<li><a href="javascript:void(0);" class="assign" data-name=""><i class="octicon octicon-x"></i> Clear assignee</a></li>
@collaborators.map { collaborator =>
<li>
<a href="javascript:void(0);" class="assign" data-name="@collaborator">
@gitbucket.core.helper.html.checkicon(issue.exists(_.assignedUserName.exists(_ == collaborator)))@helpers.avatar(collaborator, 20) @collaborator
<a href="javascript:void(0);" class="toggle-assign" data-name="@collaborator">
@gitbucket.core.helper.html.checkicon(issueAssignees.exists(_.assigneeUserName == collaborator))@helpers.avatar(collaborator, 20) @collaborator
</a>
</li>
}
@@ -140,14 +140,15 @@
}
</div>
<span id="label-assigned">
@issue.flatMap(_.assignedUserName).map { userName =>
@helpers.avatarLink(userName, 20) @helpers.user(userName, styleClass="username strong small")
}.getOrElse{
<span class="muted small">No one</span>
@issueAssignees.map { asignee =>
@helpers.avatarLink(asignee.assigneeUserName, 20) @helpers.user(asignee.assigneeUserName, styleClass="username strong small")
}
@if(issueAssignees.isEmpty) {
<span class="muted small">No one assigned</span>
}
</span>
@if(issue.isEmpty){
<input type="hidden" name="assignedUserName" value=""/>
<input type="hidden" name="assigneeUserNames" value=""/>
}
@customFields.map { case (field, value) =>
@@ -188,7 +189,7 @@
$(function(){
@issue.map { issue =>
$('a.toggle-label').click(function(){
const path = switchLabel($(this));
const path = switchToggleOptions($(this));
$.post('@helpers.url(repository)/issues/@issue.issueId/label/' + path,
{ labelId : $(this).data('label-id') },
function(data){
@@ -223,19 +224,25 @@ $(function(){
);
});
$('a.assign').click(function(){
const $this = $(this);
const userName = $this.data('name');
$.post('@helpers.url(repository)/issues/@issue.issueId/assign',
{ assignedUserName: userName },
function(){
displayAssignee($this, userName);
$('a.toggle-assign').click(function(){
const path = switchToggleOptions($(this));
$.post('@helpers.url(repository)/issues/@issue.issueId/assignee/' + path,
{ assigneeUserName : $(this).data('name') },
function(data){
const assignees = Array();
$('a.toggle-assign').each(function(i, e){
if($(e).children('i').hasClass('octicon-check') == true){
assignees.push($(e).text().trim());
}
});
displayAssignee(assignees);
}
);
return false;
});
}.getOrElse {
$('a.toggle-label').click(function(){
switchLabel($(this));
switchToggleOptions($(this));
const labelNames = Array();
$('a.toggle-label').each(function(i, e){
if($(e).children('i').hasClass('octicon-check') == true){
@@ -269,15 +276,20 @@ $(function(){
$('input[name=priorityId]').val(priorityId);
});
$('a.assign').click(function(){
const $this = $(this);
const userName = $this.data('name');
displayAssignee($this, userName);
$('input[name=assignedUserName]').val(userName);
$('a.toggle-assign').click(function(){
switchToggleOptions($(this));
const assignees = Array();
$('a.toggle-assign').each(function(i, e){
if($(e).children('i').hasClass('octicon-check') == true){
assignees.push($(e).text().trim());
}
});
$('input[name=assigneeUserNames]').val(assignees.join(','));
displayAssignee(assignees);
});
}
function switchLabel($this){
function switchToggleOptions($this){
const i = $this.children('i');
if(i.hasClass('octicon-check')){
i.removeClass('octicon-check');
@@ -320,15 +332,18 @@ $(function(){
}
}
function displayAssignee($this, userName){
function displayAssignee(assignees){
$('a.assign i.octicon-check').removeClass('octicon-check');
if(userName == ''){
$('#label-assigned').html($('<span class="muted small">').text('No one'));
if(assignees.length == 0){
$('#label-assigned').html($('<span class="muted small">').text('No one assigned'));
} else {
$('#label-assigned').empty()
.append($this.find('img.avatar-mini').clone(false)).append(' ')
.append($('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName));
$('a.assign[data-name=' + jqSelectorEscape(userName.toString()) + '] i').addClass('octicon-check');
$('#label-assigned').empty();
for (const userName of assignees) {
$('#label-assigned').append($('<div>').append(
$('a.toggle-assign').parent().find("img.avatar-mini[alt='@@" + userName + "']").clone(false),
' ',
$('<a class="username strong small">').attr('href', '@context.path/' + userName).text(userName)));
}
}
}
});

View File

@@ -230,9 +230,11 @@
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right small">
@*
@issue.assignedUserName.map { userName =>
@helpers.avatar(userName, 20, tooltip = true)
}
*@
@if(commentCount > 0){
<a href="@context.path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<i class="octicon octicon-comment active"></i> @commentCount

View File

@@ -90,9 +90,11 @@
<span class="label-color small" style="background-color: #@label.color; color: #@label.fontColor; padding-left: 4px; padding-right: 4px">@label.labelName</span>
}
<span class="pull-right small">
@*
@issue.assignedUserName.map { userName =>
@helpers.avatar(userName, 20, tooltip = true)
}
*@
@if(commentCount > 0){
<a href="@context.path/@issue.userName/@issue.repositoryName/issues/@issue.issueId" class="issue-comment-count">
<i class="octicon octicon-comment active"></i> @commentCount

View File

@@ -106,6 +106,7 @@
issue = None,
comments = Nil,
issueLabels = Nil,
issueAssignees = Nil,
collaborators = collaborators,
milestones = milestones.map((_, 0, 0)),
priorities = priorities,

View File

@@ -4,6 +4,7 @@
comments: Seq[gitbucket.core.model.Comment],
changedFileSize: Int,
issueLabels: List[gitbucket.core.model.Label],
issueAsignees: List[gitbucket.core.model.IssueAssignee],
collaborators: List[String],
milestones: List[(gitbucket.core.model.Milestone, Int, Int)],
priorities: List[gitbucket.core.model.Priority],
@@ -55,6 +56,7 @@
Some(issue),
comments.toList,
issueLabels,
issueAsignees,
collaborators,
milestones,
priorities,

View File

@@ -116,7 +116,6 @@ object ApiSpecModels {
openedUserName = "bear",
milestoneId = None,
priorityId = None,
assignedUserName = None,
title = "Found a bug",
content = Some("I'm having a problem with this."),
closed = false,
@@ -226,7 +225,7 @@ object ApiSpecModels {
issue = issue,
repositoryName = repo1Name,
user = apiUser,
assignee = Some(apiUser),
assignees = List(apiUser),
labels = List(apiLabel),
milestone = Some(apiMilestone)
)
@@ -235,7 +234,7 @@ object ApiSpecModels {
issue = issue,
repositoryName = repo1Name,
user = apiUser,
assignee = None,
assignees = List.empty,
labels = List(apiLabel),
milestone = Some(apiMilestone)
)
@@ -244,7 +243,7 @@ object ApiSpecModels {
issue = issuePR,
repositoryName = repo1Name,
user = apiUser,
assignee = Some(apiUser),
assignees = List(apiUser),
labels = List(apiLabel),
milestone = Some(apiMilestone)
)
@@ -272,7 +271,7 @@ object ApiSpecModels {
baseRepo = apiRepository,
user = apiUser,
labels = List(apiLabel),
assignee = Some(apiUser),
assignees = List(apiUser),
mergedComment = Some((issueComment, account))
)
@@ -540,7 +539,7 @@ object ApiSpecModels {
|"number":1347,
|"title":"Found a bug",
|"user":$jsonUser,
|"assignee":$jsonUser,
|"assignees":[$jsonUser],
|"labels":[$jsonLabel],
|"state":"open",
|"created_at":"2011-04-14T16:00:49Z",
@@ -548,7 +547,7 @@ object ApiSpecModels {
|"body":"I'm having a problem with this.",
|"milestone":$jsonMilestone,
|"id":0,
|"assignees":[$jsonUser],
|"assignee":$jsonUser,
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347"
|}""".stripMargin
@@ -557,6 +556,7 @@ object ApiSpecModels {
|"number":1347,
|"title":"Found a bug",
|"user":$jsonUser,
|"assignees":[],
|"labels":[$jsonLabel],
|"state":"open",
|"created_at":"2011-04-14T16:00:49Z",
@@ -564,7 +564,6 @@ object ApiSpecModels {
|"body":"I'm having a problem with this.",
|"milestone":$jsonMilestone,
|"id":0,
|"assignees":[],
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/issues/1347"
|}""".stripMargin
@@ -573,7 +572,7 @@ object ApiSpecModels {
|"number":1347,
|"title":"new-feature",
|"user":$jsonUser,
|"assignee":$jsonUser,
|"assignees":[$jsonUser],
|"labels":[$jsonLabel],
|"state":"closed",
|"created_at":"2011-04-14T16:00:49Z",
@@ -581,12 +580,12 @@ object ApiSpecModels {
|"body":"Please pull these awesome changes",
|"milestone":$jsonMilestone,
|"id":0,
|"assignees":[$jsonUser],
|"assignee":$jsonUser,
|"comments_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/issues/1347/comments",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347",
|"pull_request":{
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347"}
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347",
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347"}
|}""".stripMargin
val jsonPullRequest = s"""{
@@ -603,9 +602,10 @@ object ApiSpecModels {
|"body":"Please pull these awesome changes",
|"user":$jsonUser,
|"labels":[$jsonLabel],
|"assignee":$jsonUser,
|"assignees":[$jsonUser],
|"draft":true,
|"id":0,
|"assignee":$jsonUser,
|"html_url":"http://gitbucket.exmple.com/octocat/Hello-World/pull/1347",
|"url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347",
|"commits_url":"http://gitbucket.exmple.com/api/v3/repos/octocat/Hello-World/pulls/1347/commits",

View File

@@ -142,7 +142,6 @@ trait ServiceSpecBase {
loginUser = loginUser,
title = "issue title",
content = None,
assignedUserName = None,
milestoneId = None,
priorityId = None,
isPullRequest = true

View File

@@ -91,7 +91,7 @@ class WebHookJsonFormatSpec extends AnyFunSuite {
action = "closed",
issue = issuePR,
issueUser = account,
assignee = Some(account),
assignees = List(account),
pullRequest = pullRequest,
headRepository = repositoryInfo,
headOwner = account,
@@ -119,7 +119,7 @@ class WebHookJsonFormatSpec extends AnyFunSuite {
commentUser = account,
repository = repositoryInfo,
repositoryUser = account,
assignedUser = Some(account),
assignees = List(account),
sender = account,
labels = List(label),
milestone = Some(apiMilestone)
@@ -140,7 +140,7 @@ class WebHookJsonFormatSpec extends AnyFunSuite {
comment = commitComment,
issue = issuePR,
issueUser = account,
assignee = Some(account),
assignees = List(account),
pullRequest = pullRequest,
headRepository = repositoryInfo,
headOwner = account,

View File

@@ -1,6 +1,5 @@
package gitbucket.core.view
import gitbucket.core.util.SyntaxSugars
import org.scalatest.funspec.AnyFunSpec
class PaginationSpec extends AnyFunSpec {