diff --git a/src/main/resources/update/gitbucket-core_4.38.xml b/src/main/resources/update/gitbucket-core_4.38.xml index 2d243ac..a6feb65 100644 --- a/src/main/resources/update/gitbucket-core_4.38.xml +++ b/src/main/resources/update/gitbucket-core_4.38.xml @@ -28,4 +28,26 @@ + + + + + + + + + + + + + + + + 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 + + + diff --git a/src/main/scala/gitbucket/core/api/ApiIssue.scala b/src/main/scala/gitbucket/core/api/ApiIssue.scala index a686cd4..88afee1 100644 --- a/src/main/scala/gitbucket/core/api/ApiIssue.scala +++ b/src/main/scala/gitbucket/core/api/ApiIssue.scala @@ -12,7 +12,7 @@ number: Int, title: String, user: ApiUser, - assignee: Option[ApiUser], + assignees: List[ApiUser], labels: List[ApiLabel], state: String, created_at: Date, @@ -21,7 +21,7 @@ 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 @@ issue: Issue, repositoryName: RepositoryName, user: ApiUser, - assignee: Option[ApiUser], + assignees: List[ApiUser], labels: List[ApiLabel], milestone: Option[ApiMilestone] ): ApiIssue = @@ -51,7 +51,7 @@ number = issue.issueId, title = issue.title, user = user, - assignee = assignee, + assignees = assignees, labels = labels, milestone = milestone, state = if (issue.closed) { "closed" } else { "open" }, diff --git a/src/main/scala/gitbucket/core/api/ApiPullRequest.scala b/src/main/scala/gitbucket/core/api/ApiPullRequest.scala index 5777bdf..6079198 100644 --- a/src/main/scala/gitbucket/core/api/ApiPullRequest.scala +++ b/src/main/scala/gitbucket/core/api/ApiPullRequest.scala @@ -21,10 +21,11 @@ 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 @@ baseRepo: ApiRepository, user: ApiUser, labels: List[ApiLabel], - assignee: Option[ApiUser], + assignees: List[ApiUser], mergedComment: Option[(IssueComment, Account)] ): ApiPullRequest = ApiPullRequest( @@ -63,7 +64,7 @@ body = issue.content.getOrElse(""), user = user, labels = labels, - assignee = assignee, + assignees = assignees, draft = Some(pullRequest.isDraft) ) diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index 61ae064..d69ee6f 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -53,7 +53,7 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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 @@ 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")) diff --git a/src/main/scala/gitbucket/core/controller/LabelsController.scala b/src/main/scala/gitbucket/core/controller/LabelsController.scala index 224a571..5701538 100644 --- a/src/main/scala/gitbucket/core/controller/LabelsController.scala +++ b/src/main/scala/gitbucket/core/controller/LabelsController.scala @@ -44,7 +44,7 @@ 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 @@ 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 @@ 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) ) diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 879d534..21481cc 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -92,7 +92,7 @@ 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 @@ 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 @@ 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 @@ 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 => diff --git a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala index 7443c3e..4026a53 100644 --- a/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositoryViewerController.scala @@ -679,7 +679,6 @@ loginUser = loginAccount.userName, title = requestBranch, content = commitMessage, - assignedUserName = None, milestoneId = None, priorityId = None, isPullRequest = true diff --git a/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala index 350ad44..9d90b3d 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiIssueControllerBase.scala @@ -29,9 +29,9 @@ 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 @@ ) 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 @@ (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 @@ 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 @@ repository, data.title, data.body, - data.assignees.headOption, + data.assignees, milestone.map(_.milestoneId), None, data.labels, @@ -103,7 +104,9 @@ 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, _) } diff --git a/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala b/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala index bc6c6b1..bdd6561 100644 --- a/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala +++ b/src/main/scala/gitbucket/core/controller/api/ApiPullRequestControllerBase.scala @@ -40,7 +40,7 @@ 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 @@ ) 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 @@ 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 @@ loginUser = context.loginAccount.get.userName, title = createPullReq.title, content = createPullReq.body, - assignedUserName = None, milestoneId = None, priorityId = None, isPullRequest = true @@ -319,8 +318,8 @@ 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 @@ 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) ) } diff --git a/src/main/scala/gitbucket/core/model/Issue.scala b/src/main/scala/gitbucket/core/model/Issue.scala index ccf2c81..9a84f17 100644 --- a/src/main/scala/gitbucket/core/model/Issue.scala +++ b/src/main/scala/gitbucket/core/model/Issue.scala @@ -27,7 +27,6 @@ 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 @@ openedUserName, milestoneId.?, priorityId.?, - assignedUserName.?, title, content.?, closed, @@ -62,7 +60,6 @@ openedUserName: String, milestoneId: Option[Int], priorityId: Option[Int], - assignedUserName: Option[String], title: String, content: Option[String], closed: Boolean, diff --git a/src/main/scala/gitbucket/core/model/IssueAssignee.scala b/src/main/scala/gitbucket/core/model/IssueAssignee.scala new file mode 100644 index 0000000..81ab4b0 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/IssueAssignee.scala @@ -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 +) diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala index c175c1f..c155de5 100644 --- a/src/main/scala/gitbucket/core/model/Profile.scala +++ b/src/main/scala/gitbucket/core/model/Profile.scala @@ -74,5 +74,6 @@ with AccountPreferenceComponent with CustomFieldComponent with IssueCustomFieldComponent + with IssueAssigneeComponent object Profile extends CoreProfile diff --git a/src/main/scala/gitbucket/core/service/IssueCreationService.scala b/src/main/scala/gitbucket/core/service/IssueCreationService.scala index c56cf0c..467c462 100644 --- a/src/main/scala/gitbucket/core/service/IssueCreationService.scala +++ b/src/main/scala/gitbucket/core/service/IssueCreationService.scala @@ -16,7 +16,7 @@ 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 @@ 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) } diff --git a/src/main/scala/gitbucket/core/service/IssuesService.scala b/src/main/scala/gitbucket/core/service/IssuesService.scala index 76eb8c0..cbe2802 100644 --- a/src/main/scala/gitbucket/core/service/IssuesService.scala +++ b/src/main/scala/gitbucket/core/service/IssuesService.scala @@ -5,7 +5,17 @@ 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 @@ 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 @@ } /** 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 @@ */ 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 @@ .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 @@ 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 @@ 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 @@ .&&(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 @@ loginUser: String, title: String, content: Option[String], - assignedUserName: Option[String], milestoneId: Option[Int], priorityId: Option[Int], isPullRequest: Boolean = false @@ -427,7 +473,6 @@ loginUser, milestoneId, priorityId, - assignedUserName, title, content, false, @@ -542,35 +587,91 @@ .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( diff --git a/src/main/scala/gitbucket/core/service/WebHookService.scala b/src/main/scala/gitbucket/core/service/WebHookService.scala index f064597..68c85ff 100644 --- a/src/main/scala/gitbucket/core/service/WebHookService.scala +++ b/src/main/scala/gitbucket/core/service/WebHookService.scala @@ -379,8 +379,12 @@ 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 @@ 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 @@ 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 @@ action = action, issue = issue, issueUser = issueUser, - assignee = assignee, + assignees = assigneeUsers, pullRequest = pullRequest, headRepository = headRepo, headOwner = headOwner, @@ -481,8 +484,8 @@ 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 @@ action = action, issue = issue, issueUser = issueUser, - assignee = assignee, + assignees = assignees, pullRequest = pullRequest, headRepository = requestRepository, headOwner = headOwner, @@ -522,15 +525,17 @@ )(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 @@ comment = comment, issue = issue, issueUser = issueUser, - assignee = assignee, + assignees = assigneeUsers, pullRequest = pullRequest, headRepository = headRepo, headOwner = headOwner, @@ -569,14 +574,17 @@ 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 @@ commentUser = commenter, repository = repository, repositoryUser = repoOwner, - assignedUser = assignedUser, + assignees = assigneeUsers, sender = sender, labels = labels, milestone = milestone @@ -710,7 +718,7 @@ action: String, issue: Issue, issueUser: Account, - assignee: Option[Account], + assignees: List[Account], pullRequest: PullRequest, headRepository: RepositoryInfo, headOwner: Account, @@ -731,7 +739,7 @@ baseRepo = baseRepoPayload, user = ApiUser(issueUser), labels = labels, - assignee = assignee.map(ApiUser.apply), + assignees = assignees.map(ApiUser.apply), mergedComment = mergedComment ) @@ -762,7 +770,7 @@ commentUser: Account, repository: RepositoryInfo, repositoryUser: Account, - assignedUser: Option[Account], + assignees: List[Account], sender: Account, labels: List[Label], milestone: Option[ApiMilestone] @@ -774,7 +782,7 @@ issue, RepositoryName(repository), ApiUser(issueUser), - assignedUser.map(ApiUser(_)), + assignees.map(ApiUser(_)), labels.map(ApiLabel(_, RepositoryName(repository))), milestone ), @@ -799,7 +807,7 @@ comment: CommitComment, issue: Issue, issueUser: Account, - assignee: Option[Account], + assignees: List[Account], pullRequest: PullRequest, headRepository: RepositoryInfo, headOwner: Account, @@ -828,7 +836,7 @@ baseRepo = baseRepoPayload, user = ApiUser(issueUser), labels = labels, - assignee = assignee.map(ApiUser.apply), + assignees = assignees.map(ApiUser.apply), mergedComment = mergedComment ), repository = baseRepoPayload, diff --git a/src/main/twirl/gitbucket/core/dashboard/issueslist.scala.html b/src/main/twirl/gitbucket/core/dashboard/issueslist.scala.html index 9105bd0..d0ef372 100644 --- a/src/main/twirl/gitbucket/core/dashboard/issueslist.scala.html +++ b/src/main/twirl/gitbucket/core/dashboard/issueslist.scala.html @@ -33,9 +33,11 @@ @label.labelName } + @* @issue.assignedUserName.map { userName => @helpers.avatar(userName, 20, tooltip = true) } + *@ @if(commentCount > 0){ @commentCount diff --git a/src/main/twirl/gitbucket/core/issues/commentlist.scala.html b/src/main/twirl/gitbucket/core/issues/commentlist.scala.html index fa1dd84..a0240e9 100644 --- a/src/main/twirl/gitbucket/core/issues/commentlist.scala.html +++ b/src/main/twirl/gitbucket/core/issues/commentlist.scala.html @@ -200,7 +200,7 @@ @helpers.avatarLink(comment.commentedUserName, 16) @helpers.user(comment.commentedUserName, styleClass="username strong") - add the @comment.content label + added the @comment.content label @gitbucket.core.helper.html.datetimeago(comment.registeredDate) @@ -222,7 +222,7 @@ @helpers.avatarLink(comment.commentedUserName, 16) @helpers.user(comment.commentedUserName, styleClass="username strong") - change priority from @comment.content.split(":")(0) to @comment.content.split(":")(1) + changed priority from @comment.content.split(":")(0) to @comment.content.split(":")(1) @gitbucket.core.helper.html.datetimeago(comment.registeredDate) @@ -233,18 +233,40 @@ @helpers.avatarLink(comment.commentedUserName, 16) @helpers.user(comment.commentedUserName, styleClass="username strong") - change milestone from @comment.content.split(":")(0) to @comment.content.split(":")(1) + changed milestone from @comment.content.split(":")(0) to @comment.content.split(":")(1) @gitbucket.core.helper.html.datetimeago(comment.registeredDate) } - case "assign" => { + case "assign" => { @* for backward compatibility *@
@helpers.avatarLink(comment.commentedUserName, 16) @helpers.user(comment.commentedUserName, styleClass="username strong") - change assignee from @comment.content.split(":")(0) to @comment.content.split(":")(1) + changed assignee from @comment.content.split(":")(0) to @comment.content.split(":")(1) + @gitbucket.core.helper.html.datetimeago(comment.registeredDate) +
+
+ } + case "add_assignee" => { +
+
+ + @helpers.avatarLink(comment.commentedUserName, 16) + @helpers.user(comment.commentedUserName, styleClass="username strong") + assigned @comment.content + @gitbucket.core.helper.html.datetimeago(comment.registeredDate) +
+
+ } + case "delete_assignee" => { +
+
+ + @helpers.avatarLink(comment.commentedUserName, 16) + @helpers.user(comment.commentedUserName, styleClass="username strong") + unassigned @comment.content @gitbucket.core.helper.html.datetimeago(comment.registeredDate)
@@ -255,7 +277,7 @@ @helpers.avatar(comment.commentedUserName, 16) @helpers.user(comment.commentedUserName, styleClass="username strong") - change title from @convertLineSeparator(comment.content, "LF").split("\n")(0) to @convertLineSeparator(comment.content, "LF").split("\n")(1) + changed title from @convertLineSeparator(comment.content, "LF").split("\n")(0) to @convertLineSeparator(comment.content, "LF").split("\n")(1) @gitbucket.core.helper.html.datetimeago(comment.registeredDate) @@ -266,7 +288,7 @@ @helpers.avatar(comment.commentedUserName, 16) @helpers.user(comment.commentedUserName, styleClass="username strong") - change base branch from @convertLineSeparator(comment.content, "LF").split("\n")(0) to @convertLineSeparator(comment.content, "LF").split("\n")(1) + changed base branch from @convertLineSeparator(comment.content, "LF").split("\n")(0) to @convertLineSeparator(comment.content, "LF").split("\n")(1) @gitbucket.core.helper.html.datetimeago(comment.registeredDate) @@ -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 @@ }); $('.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 @@ }); } $(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 @@ $(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 @@ } // 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 @@ }); $('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 @@ ); }); - @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 @@ }); $('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 @@ } ); }); - - } - +} }); } diff --git a/src/main/twirl/gitbucket/core/issues/create.scala.html b/src/main/twirl/gitbucket/core/issues/create.scala.html index 045acbe..3f9e445 100644 --- a/src/main/twirl/gitbucket/core/issues/create.scala.html +++ b/src/main/twirl/gitbucket/core/issues/create.scala.html @@ -36,6 +36,7 @@ issue = None, comments = Nil, issueLabels = Nil, + issueAssignees = Nil, collaborators = collaborators, milestones = milestones.map(x => (x, 0, 0)), priorities= priorities, diff --git a/src/main/twirl/gitbucket/core/issues/issue.scala.html b/src/main/twirl/gitbucket/core/issues/issue.scala.html index 087206a..7217135 100644 --- a/src/main/twirl/gitbucket/core/issues/issue.scala.html +++ b/src/main/twirl/gitbucket/core/issues/issue.scala.html @@ -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, diff --git a/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html b/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html index d1de4c3..1985f66 100644 --- a/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html +++ b/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html @@ -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){
- @issue.flatMap(_.assignedUserName).map { userName => - @helpers.avatarLink(userName, 20) @helpers.user(userName, styleClass="username strong small") - }.getOrElse{ - No one + @issueAssignees.map { asignee => + @helpers.avatarLink(asignee.assigneeUserName, 20) @helpers.user(asignee.assigneeUserName, styleClass="username strong small") + } + @if(issueAssignees.isEmpty) { + No one assigned } @if(issue.isEmpty){ - + } @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 @@ ); }); - $('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 @@ $('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 displayAssignee($this, userName){ + function displayAssignee(assignees){ $('a.assign i.octicon-check').removeClass('octicon-check'); - if(userName == ''){ - $('#label-assigned').html($('').text('No one')); + if(assignees.length == 0){ + $('#label-assigned').html($('').text('No one assigned')); } else { - $('#label-assigned').empty() - .append($this.find('img.avatar-mini').clone(false)).append(' ') - .append($('').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($('
').append( + $('a.toggle-assign').parent().find("img.avatar-mini[alt='@@" + userName + "']").clone(false), + ' ', + $('').attr('href', '@context.path/' + userName).text(userName))); + } } } }); diff --git a/src/main/twirl/gitbucket/core/issues/listparts.scala.html b/src/main/twirl/gitbucket/core/issues/listparts.scala.html index 1a7fc68..677cc0e 100644 --- a/src/main/twirl/gitbucket/core/issues/listparts.scala.html +++ b/src/main/twirl/gitbucket/core/issues/listparts.scala.html @@ -230,9 +230,11 @@ @label.labelName } + @* @issue.assignedUserName.map { userName => @helpers.avatar(userName, 20, tooltip = true) } + *@ @if(commentCount > 0){ @commentCount diff --git a/src/main/twirl/gitbucket/core/issues/milestones/listparts.scala.html b/src/main/twirl/gitbucket/core/issues/milestones/listparts.scala.html index 7eb98a5..cdbff56 100644 --- a/src/main/twirl/gitbucket/core/issues/milestones/listparts.scala.html +++ b/src/main/twirl/gitbucket/core/issues/milestones/listparts.scala.html @@ -90,9 +90,11 @@ @label.labelName } + @* @issue.assignedUserName.map { userName => @helpers.avatar(userName, 20, tooltip = true) } + *@ @if(commentCount > 0){ @commentCount diff --git a/src/main/twirl/gitbucket/core/pulls/compare.scala.html b/src/main/twirl/gitbucket/core/pulls/compare.scala.html index abcbfb0..6f0e22b 100644 --- a/src/main/twirl/gitbucket/core/pulls/compare.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/compare.scala.html @@ -106,6 +106,7 @@ issue = None, comments = Nil, issueLabels = Nil, + issueAssignees = Nil, collaborators = collaborators, milestones = milestones.map((_, 0, 0)), priorities = priorities, diff --git a/src/main/twirl/gitbucket/core/pulls/conversation.scala.html b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html index c4b9f48..223cb78 100644 --- a/src/main/twirl/gitbucket/core/pulls/conversation.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html @@ -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, diff --git a/src/test/scala/gitbucket/core/api/ApiSpecModels.scala b/src/test/scala/gitbucket/core/api/ApiSpecModels.scala index 42e35c1..2f966fd 100644 --- a/src/test/scala/gitbucket/core/api/ApiSpecModels.scala +++ b/src/test/scala/gitbucket/core/api/ApiSpecModels.scala @@ -116,7 +116,6 @@ 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 @@ issue = issue, repositoryName = repo1Name, user = apiUser, - assignee = Some(apiUser), + assignees = List(apiUser), labels = List(apiLabel), milestone = Some(apiMilestone) ) @@ -235,7 +234,7 @@ issue = issue, repositoryName = repo1Name, user = apiUser, - assignee = None, + assignees = List.empty, labels = List(apiLabel), milestone = Some(apiMilestone) ) @@ -244,7 +243,7 @@ issue = issuePR, repositoryName = repo1Name, user = apiUser, - assignee = Some(apiUser), + assignees = List(apiUser), labels = List(apiLabel), milestone = Some(apiMilestone) ) @@ -272,7 +271,7 @@ baseRepo = apiRepository, user = apiUser, labels = List(apiLabel), - assignee = Some(apiUser), + assignees = List(apiUser), mergedComment = Some((issueComment, account)) ) @@ -540,7 +539,7 @@ |"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 @@ |"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 @@ |"number":1347, |"title":"Found a bug", |"user":$jsonUser, + |"assignees":[], |"labels":[$jsonLabel], |"state":"open", |"created_at":"2011-04-14T16:00:49Z", @@ -564,7 +564,6 @@ |"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 @@ |"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 @@ |"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 @@ |"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", diff --git a/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala index 65d36a8..bda0c25 100644 --- a/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala +++ b/src/test/scala/gitbucket/core/service/ServiceSpecBase.scala @@ -142,7 +142,6 @@ loginUser = loginUser, title = "issue title", content = None, - assignedUserName = None, milestoneId = None, priorityId = None, isPullRequest = true diff --git a/src/test/scala/gitbucket/core/service/WebHookJsonFormatSpec.scala b/src/test/scala/gitbucket/core/service/WebHookJsonFormatSpec.scala index 11deadc..87e22df 100644 --- a/src/test/scala/gitbucket/core/service/WebHookJsonFormatSpec.scala +++ b/src/test/scala/gitbucket/core/service/WebHookJsonFormatSpec.scala @@ -91,7 +91,7 @@ action = "closed", issue = issuePR, issueUser = account, - assignee = Some(account), + assignees = List(account), pullRequest = pullRequest, headRepository = repositoryInfo, headOwner = account, @@ -119,7 +119,7 @@ commentUser = account, repository = repositoryInfo, repositoryUser = account, - assignedUser = Some(account), + assignees = List(account), sender = account, labels = List(label), milestone = Some(apiMilestone) @@ -140,7 +140,7 @@ comment = commitComment, issue = issuePR, issueUser = account, - assignee = Some(account), + assignees = List(account), pullRequest = pullRequest, headRepository = repositoryInfo, headOwner = account, diff --git a/src/test/scala/gitbucket/core/view/PaginationSpec.scala b/src/test/scala/gitbucket/core/view/PaginationSpec.scala index 1b468fc..93becdc 100644 --- a/src/test/scala/gitbucket/core/view/PaginationSpec.scala +++ b/src/test/scala/gitbucket/core/view/PaginationSpec.scala @@ -1,6 +1,5 @@ package gitbucket.core.view -import gitbucket.core.util.SyntaxSugars import org.scalatest.funspec.AnyFunSpec class PaginationSpec extends AnyFunSpec {