diff --git a/src/main/resources/update/gitbucket-core_4.38.xml b/src/main/resources/update/gitbucket-core_4.38.xml new file mode 100644 index 0000000..2d243ac --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.38.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index a863c81..b602921 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -122,5 +122,6 @@ new Version("4.36.2"), new Version("4.37.0", new LiquibaseMigration("update/gitbucket-core_4.37.xml")), new Version("4.37.1"), - new Version("4.37.2") + new Version("4.37.2"), + new Version("4.38.0", new LiquibaseMigration("update/gitbucket-core_4.38.xml")) ) diff --git a/src/main/scala/gitbucket/core/controller/IssuesController.scala b/src/main/scala/gitbucket/core/controller/IssuesController.scala index ebe7c3b..3ee85c8 100644 --- a/src/main/scala/gitbucket/core/controller/IssuesController.scala +++ b/src/main/scala/gitbucket/core/controller/IssuesController.scala @@ -1,7 +1,7 @@ package gitbucket.core.controller import gitbucket.core.issues.html -import gitbucket.core.model.Account +import gitbucket.core.model.{Account, CustomFieldBehavior} import gitbucket.core.service.IssuesService._ import gitbucket.core.service._ import gitbucket.core.util.Implicits._ @@ -21,6 +21,7 @@ with ActivityService with HandleCommentService with IssueCreationService + with CustomFieldsService with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator @@ -41,6 +42,7 @@ with ActivityService with HandleCommentService with IssueCreationService + with CustomFieldsService with ReadableUsersAuthenticator with ReferrerAuthenticator with WritableUsersAuthenticator @@ -109,6 +111,7 @@ getMilestonesWithIssueCount(repository.owner, repository.name), getPriorities(repository.owner, repository.name), getLabels(repository.owner, repository.name), + getCustomFieldsWithValue(repository.owner, repository.name, issueId.toInt).filter(_._1.enableForIssues), isIssueEditable(repository), isIssueManageable(repository), isIssueCommentManageable(repository), @@ -126,6 +129,7 @@ getPriorities(repository.owner, repository.name), getDefaultPriority(repository.owner, repository.name), getLabels(repository.owner, repository.name), + getCustomFields(repository.owner, repository.name).filter(_.enableForIssues), isIssueManageable(repository), getContentTemplate(repository, "ISSUE_TEMPLATE"), repository @@ -147,6 +151,25 @@ form.labelNames.toSeq.flatMap(_.split(",")), loginAccount ) + + // Insert custom field values + params.toMap.foreach { + case (key, value) => + if (key.startsWith("custom-field-")) { + getCustomField( + repository.owner, + repository.name, + key.replaceFirst("^custom-field-", "").toInt + ).foreach { field => + CustomFieldBehavior.validate(field, value, messages) match { + case None => + insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issue.issueId, value) + case Some(_) => halt(400) + } + } + } + } + redirect(s"/${issue.userName}/${issue.repositoryName}/issues/${issue.issueId}") } else Unauthorized() } @@ -362,6 +385,35 @@ Ok("updated") }) + ajaxPost("/:owner/:repository/issues/customfield_validation/:fieldId")(writableUsersOnly { repository => + val fieldId = params("fieldId").toInt + val value = params("value") + getCustomField(repository.owner, repository.name, fieldId) + .flatMap { field => + CustomFieldBehavior.validate(field, value, messages).map { error => + Ok(error) + } + } + .getOrElse(Ok()) + }) + + ajaxPost("/:owner/:repository/issues/:id/customfield/:fieldId")(writableUsersOnly { repository => + val issueId = params("id").toInt + val fieldId = params("fieldId").toInt + val value = params("value") + + for { + _ <- getIssue(repository.owner, repository.name, issueId.toString) + field <- getCustomField(repository.owner, repository.name, fieldId) + } { + CustomFieldBehavior.validate(field, value, messages) match { + case None => insertOrUpdateCustomFieldValue(field, repository.owner, repository.name, issueId, value) + case Some(_) => halt(400) + } + } + Ok(value) + }) + post("/:owner/:repository/issues/batchedit/state")(writableUsersOnly { repository => val action = params.get("value") action match { diff --git a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala index 784844d..879d534 100644 --- a/src/main/scala/gitbucket/core/controller/PullRequestsController.scala +++ b/src/main/scala/gitbucket/core/controller/PullRequestsController.scala @@ -25,6 +25,7 @@ with PullRequestService with MilestonesService with LabelsService + with CustomFieldsService with CommitsService with ActivityService with WebHookPullRequestService @@ -44,6 +45,7 @@ with IssuesService with MilestonesService with LabelsService + with CustomFieldsService with CommitsService with ActivityService with PullRequestService @@ -133,6 +135,7 @@ getMilestonesWithIssueCount(repository.owner, repository.name), getPriorities(repository.owner, repository.name), getLabels(repository.owner, repository.name), + getCustomFieldsWithValue(repository.owner, repository.name, issueId).filter(_._1.enableForPullRequests), isEditable(repository), isManageable(repository), hasDeveloperRole(pullreq.requestUserName, pullreq.requestRepositoryName, context.loginAccount), @@ -505,7 +508,8 @@ getMilestones(originRepository.owner, originRepository.name), getPriorities(originRepository.owner, originRepository.name), getDefaultPriority(originRepository.owner, originRepository.name), - getLabels(originRepository.owner, originRepository.name) + getLabels(originRepository.owner, originRepository.name), + getCustomFields(originRepository.owner, originRepository.name).filter(_.enableForPullRequests) ) } case (oldId, newId) => diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index 9b243af..a949ba6 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -2,7 +2,6 @@ import java.time.{LocalDateTime, ZoneOffset} import java.util.Date - import gitbucket.core.settings.html import gitbucket.core.model.{RepositoryWebHook, WebHook} import gitbucket.core.service._ @@ -21,7 +20,7 @@ import org.eclipse.jgit.lib.ObjectId import scala.util.Using -import org.scalatra.Forbidden +import org.scalatra.{Forbidden, Ok} class RepositorySettingsController extends RepositorySettingsControllerBase @@ -31,6 +30,7 @@ with ProtectedBranchService with CommitStatusService with DeployKeyService + with CustomFieldsService with ActivityService with OwnerAuthenticator with UsersAuthenticator @@ -43,6 +43,7 @@ with ProtectedBranchService with CommitStatusService with DeployKeyService + with CustomFieldsService with ActivityService with OwnerAuthenticator with UsersAuthenticator => @@ -121,6 +122,21 @@ "newOwner" -> trim(label("New owner", text(required, transferUser))) )(TransferOwnerShipForm.apply) + // for custom field + case class CustomFieldForm( + fieldName: String, + fieldType: String, + enableForIssues: Boolean, + enableForPullRequests: Boolean + ) + + val customFieldForm = mapping( + "fieldName" -> trim(label("Field name", text(required, maxlength(100)))), + "fieldType" -> trim(label("Field type", text(required))), + "enableForIssues" -> trim(label("Enable for issues", boolean(required))), + "enableForPullRequests" -> trim(label("Enable for pull requests", boolean(required))), + )(CustomFieldForm.apply) + /** * Redirect to the Options page. */ @@ -477,6 +493,58 @@ redirect(s"/${repository.owner}/${repository.name}/settings/deploykey") }) + /** Custom fields for issues and pull requests */ + get("/:owner/:repository/settings/issues")(ownerOnly { repository => + val customFields = getCustomFields(repository.owner, repository.name) + html.issues(customFields, repository) + }) + + /** New custom field form */ + get("/:owner/:repository/settings/issues/fields/new")(ownerOnly { repository => + html.issuesfieldform(None, repository) + }) + + /** Add custom field */ + ajaxPost("/:owner/:repository/settings/issues/fields/new", customFieldForm)(ownerOnly { (form, repository) => + val fieldId = createCustomField( + repository.owner, + repository.name, + form.fieldName, + form.fieldType, + form.enableForIssues, + form.enableForPullRequests + ) + html.issuesfield(getCustomField(repository.owner, repository.name, fieldId).get) + }) + + /** Edit custom field form */ + ajaxGet("/:owner/:repository/settings/issues/fields/:fieldId/edit")(ownerOnly { repository => + getCustomField(repository.owner, repository.name, params("fieldId").toInt).map { customField => + html.issuesfieldform(Some(customField), repository) + } getOrElse NotFound() + }) + + /** Update custom field */ + ajaxPost("/:owner/:repository/settings/issues/fields/:fieldId/edit", customFieldForm)(ownerOnly { + (form, repository) => + updateCustomField( + repository.owner, + repository.name, + params("fieldId").toInt, + form.fieldName, + form.fieldType, + form.enableForIssues, + form.enableForPullRequests + ) + html.issuesfield(getCustomField(repository.owner, repository.name, params("fieldId").toInt).get) + }) + + /** Delete custom field */ + ajaxPost("/:owner/:repository/settings/issues/fields/:fieldId/delete")(ownerOnly { repository => + deleteCustomField(repository.owner, repository.name, params("fieldId").toInt) + Ok() + }) + /** * Provides duplication check for web hook url. */ diff --git a/src/main/scala/gitbucket/core/model/CustomField.scala b/src/main/scala/gitbucket/core/model/CustomField.scala new file mode 100644 index 0000000..4d5a4f1 --- /dev/null +++ b/src/main/scala/gitbucket/core/model/CustomField.scala @@ -0,0 +1,191 @@ +package gitbucket.core.model + +import gitbucket.core.controller.Context +import gitbucket.core.service.RepositoryService.RepositoryInfo +import gitbucket.core.util.StringUtil +import gitbucket.core.view.helpers +import org.scalatra.i18n.Messages + +trait CustomFieldComponent extends TemplateComponent { self: Profile => + import profile.api._ + + lazy val CustomFields = TableQuery[CustomFields] + + class CustomFields(tag: Tag) extends Table[CustomField](tag, "CUSTOM_FIELD") with BasicTemplate { + val fieldId = column[Int]("FIELD_ID", O AutoInc) + val fieldName = column[String]("FIELD_NAME") + val fieldType = column[String]("FIELD_TYPE") + val enableForIssues = column[Boolean]("ENABLE_FOR_ISSUES") + val enableForPullRequests = column[Boolean]("ENABLE_FOR_PULL_REQUESTS") + def * = + (userName, repositoryName, fieldId, fieldName, fieldType, enableForIssues, enableForPullRequests) + .<>(CustomField.tupled, CustomField.unapply) + + def byPrimaryKey(userName: String, repositoryName: String, fieldId: Int) = + (this.userName === userName.bind) && (this.repositoryName === repositoryName.bind) && (this.fieldId === fieldId.bind) + } +} + +case class CustomField( + userName: String, + repositoryName: String, + fieldId: Int = 0, + fieldName: String, + fieldType: String, // long, double, string, or date + enableForIssues: Boolean, + enableForPullRequests: Boolean +) + +trait CustomFieldBehavior { + def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit conext: Context): String + def fieldHtml(repository: RepositoryInfo, issueId: Int, fieldId: Int, value: String, editable: Boolean)( + implicit context: Context + ): String + def validate(name: String, value: String, messages: Messages): Option[String] +} + +object CustomFieldBehavior { + def validate(field: CustomField, value: String, messages: Messages): Option[String] = { + if (value.isEmpty) None + else { + CustomFieldBehavior(field.fieldType).flatMap { behavior => + behavior.validate(field.fieldName, value, messages) + } + } + } + + def apply(fieldType: String): Option[CustomFieldBehavior] = { + fieldType match { + case "long" => Some(LongFieldBehavior) + case "double" => Some(DoubleFieldBehavior) + case "string" => Some(StringFieldBehavior) + case "date" => Some(DateFieldBehavior) + case _ => None + } + } + + case object LongFieldBehavior extends TextFieldBehavior { + override def validate(name: String, value: String, messages: Messages): Option[String] = { + try { + value.toLong + None + } catch { + case _: NumberFormatException => Some(messages("error.number").format(name)) + } + } + } + case object DoubleFieldBehavior extends TextFieldBehavior { + override def validate(name: String, value: String, messages: Messages): Option[String] = { + try { + value.toDouble + None + } catch { + case _: NumberFormatException => Some(messages("error.number").format(name)) + } + } + } + case object StringFieldBehavior extends TextFieldBehavior + case object DateFieldBehavior extends TextFieldBehavior { + private val pattern = "yyyy-MM-dd" + override protected val fieldType: String = "date" + + override def validate(name: String, value: String, messages: Messages): Option[String] = { + try { + new java.text.SimpleDateFormat(pattern).parse(value) + None + } catch { + case _: java.text.ParseException => + Some(messages("error.datePattern").format(name, pattern)) + } + } + } + + trait TextFieldBehavior extends CustomFieldBehavior { + protected val fieldType = "text" + + def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit context: Context): String = { + val sb = new StringBuilder + sb.append( + s"""""" + ) + sb.append(s""" + |""".stripMargin) + sb.toString() + } + + def fieldHtml(repository: RepositoryInfo, issueId: Int, fieldId: Int, value: String, editable: Boolean)( + implicit context: Context + ): String = { + val sb = new StringBuilder + sb.append( + s"""${StringUtil + .escapeHtml(value)}""".stripMargin + ) + if (editable) { + sb.append( + s"""""" + ) + sb.append(s""" + |""".stripMargin) + } + sb.toString() + } + + def validate(name: String, value: String, messages: Messages): Option[String] = None + } +} diff --git a/src/main/scala/gitbucket/core/model/IssueCustomFields.scala b/src/main/scala/gitbucket/core/model/IssueCustomFields.scala new file mode 100644 index 0000000..5ec6b9d --- /dev/null +++ b/src/main/scala/gitbucket/core/model/IssueCustomFields.scala @@ -0,0 +1,31 @@ +package gitbucket.core.model + +trait IssueCustomFieldComponent extends TemplateComponent { self: Profile => + import profile.api._ + import self._ + + lazy val IssueCustomFields = TableQuery[IssueCustomFields] + + class IssueCustomFields(tag: Tag) extends Table[IssueCustomField](tag, "ISSUE_CUSTOM_FIELD") { + val userName = column[String]("USER_NAME", O.PrimaryKey) + val repositoryName = column[String]("REPOSITORY_NAME", O.PrimaryKey) + val issueId = column[Int]("ISSUE_ID", O.PrimaryKey) + val fieldId = column[Int]("FIELD_ID", O.PrimaryKey) + val value = column[String]("VALUE") + def * = + (userName, repositoryName, issueId, fieldId, value) + .<>(IssueCustomField.tupled, IssueCustomField.unapply) + + def byPrimaryKey(owner: String, repository: String, issueId: Int, fieldId: Int) = { + this.userName === owner.bind && this.repositoryName === repository.bind && this.issueId === issueId.bind && this.fieldId === fieldId.bind + } + } +} + +case class IssueCustomField( + userName: String, + repositoryName: String, + issueId: Int, + fieldId: Int, + value: String +) diff --git a/src/main/scala/gitbucket/core/model/Profile.scala b/src/main/scala/gitbucket/core/model/Profile.scala index 02bb2bd..c175c1f 100644 --- a/src/main/scala/gitbucket/core/model/Profile.scala +++ b/src/main/scala/gitbucket/core/model/Profile.scala @@ -72,5 +72,7 @@ with ReleaseAssetComponent with AccountExtraMailAddressComponent with AccountPreferenceComponent + with CustomFieldComponent + with IssueCustomFieldComponent object Profile extends CoreProfile diff --git a/src/main/scala/gitbucket/core/service/CustomFieldsService.scala b/src/main/scala/gitbucket/core/service/CustomFieldsService.scala new file mode 100644 index 0000000..cc5ddfa --- /dev/null +++ b/src/main/scala/gitbucket/core/service/CustomFieldsService.scala @@ -0,0 +1,94 @@ +package gitbucket.core.service + +import gitbucket.core.model.{CustomField, IssueCustomField} +import gitbucket.core.model.Profile._ +import gitbucket.core.model.Profile.profile.blockingApi._ + +trait CustomFieldsService { + + def getCustomFields(owner: String, repository: String)(implicit s: Session): List[CustomField] = + CustomFields.filter(_.byRepository(owner, repository)).sortBy(_.fieldId asc).list + + def getCustomFieldsWithValue(owner: String, repository: String, issueId: Int)( + implicit s: Session + ): List[(CustomField, Option[IssueCustomField])] = { + CustomFields + .filter(_.byRepository(owner, repository)) + .joinLeft(IssueCustomFields) + .on { case (t1, t2) => t1.fieldId === t2.fieldId && t2.issueId === issueId.bind } + .sortBy { case (t1, t2) => t1.fieldId } + .list + } + + def getCustomField(owner: String, repository: String, fieldId: Int)(implicit s: Session): Option[CustomField] = + CustomFields.filter(_.byPrimaryKey(owner, repository, fieldId)).firstOption + + def createCustomField( + owner: String, + repository: String, + fieldName: String, + fieldType: String, + enableForIssues: Boolean, + enableForPullRequests: Boolean + )(implicit s: Session): Int = { + CustomFields returning CustomFields.map(_.fieldId) insert CustomField( + userName = owner, + repositoryName = repository, + fieldName = fieldName, + fieldType = fieldType, + enableForIssues = enableForIssues, + enableForPullRequests = enableForPullRequests + ) + } + + def updateCustomField( + owner: String, + repository: String, + fieldId: Int, + fieldName: String, + fieldType: String, + enableForIssues: Boolean, + enableForPullRequests: Boolean + )( + implicit s: Session + ): Unit = + CustomFields + .filter(_.byPrimaryKey(owner, repository, fieldId)) + .map(t => (t.fieldName, t.fieldType, t.enableForIssues, t.enableForPullRequests)) + .update((fieldName, fieldType, enableForIssues, enableForPullRequests)) + + def deleteCustomField(owner: String, repository: String, fieldId: Int)(implicit s: Session): Unit = { + IssueCustomFields + .filter(t => t.userName === owner.bind && t.repositoryName === repository.bind && t.fieldId === fieldId.bind) + .delete + CustomFields.filter(_.byPrimaryKey(owner, repository, fieldId)).delete + } + + def getCustomFieldValues( + userName: String, + repositoryName: String, + issueId: Int, + )(implicit s: Session): List[IssueCustomField] = { + IssueCustomFields + .filter(t => t.userName === userName && t.repositoryName === repositoryName.bind && t.issueId === issueId.bind) + .list + } + + def insertOrUpdateCustomFieldValue( + field: CustomField, + userName: String, + repositoryName: String, + issueId: Int, + value: String + )(implicit s: Session): Unit = { + IssueCustomFields.insertOrUpdate( + IssueCustomField( + userName = userName, + repositoryName = repositoryName, + issueId = issueId, + fieldId = field.fieldId, + value = value + ) + ) + } +} diff --git a/src/main/twirl/gitbucket/core/issues/create.scala.html b/src/main/twirl/gitbucket/core/issues/create.scala.html index 79db49e..045acbe 100644 --- a/src/main/twirl/gitbucket/core/issues/create.scala.html +++ b/src/main/twirl/gitbucket/core/issues/create.scala.html @@ -3,6 +3,7 @@ priorities: List[gitbucket.core.model.Priority], defaultPriority: Option[gitbucket.core.model.Priority], labels: List[gitbucket.core.model.Label], + customFields: List[gitbucket.core.model.CustomField], isManageable: Boolean, content: String, repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) @@ -27,13 +28,36 @@ elastic = true )
- +
- @gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map(x => (x, 0, 0)), priorities, defaultPriority, labels, isManageable, repository) + @gitbucket.core.issues.html.issueinfo( + issue = None, + comments = Nil, + issueLabels = Nil, + collaborators = collaborators, + milestones = milestones.map(x => (x, 0, 0)), + priorities= priorities, + defaultPriority = defaultPriority, + labels = labels, + customFields = customFields.map((_, None)), + isManageable = isManageable, + repository = repository + )
+ } } diff --git a/src/main/twirl/gitbucket/core/issues/issue.scala.html b/src/main/twirl/gitbucket/core/issues/issue.scala.html index 79072b4..087206a 100644 --- a/src/main/twirl/gitbucket/core/issues/issue.scala.html +++ b/src/main/twirl/gitbucket/core/issues/issue.scala.html @@ -5,6 +5,7 @@ milestones: List[(gitbucket.core.model.Milestone, Int, Int)], priorities: List[gitbucket.core.model.Priority], labels: List[gitbucket.core.model.Label], + customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])], isEditable: Boolean, isManageable: Boolean, isCommentManageable: Boolean, @@ -56,7 +57,19 @@ @gitbucket.core.issues.html.commentform(issue, true, isEditable, isManageable, repository)
- @gitbucket.core.issues.html.issueinfo(Some(issue), comments, issueLabels, collaborators, milestones, priorities, None, labels, isManageable, repository) + @gitbucket.core.issues.html.issueinfo( + issue = Some(issue), + comments = comments, + issueLabels = issueLabels, + collaborators = collaborators, + milestones = milestones, + priorities = priorities, + defaultPriority = None, + labels = labels, + customFields = customFields, + isManageable = isManageable, + repository = repository + )
} diff --git a/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html b/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html index fc0286f..d1de4c3 100644 --- a/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html +++ b/src/main/twirl/gitbucket/core/issues/issueinfo.scala.html @@ -1,3 +1,4 @@ +@import org.json4s.scalap.scalasig.ClassFileParser.field @(issue: Option[gitbucket.core.model.Issue], comments: List[gitbucket.core.model.Comment], issueLabels: List[gitbucket.core.model.Label], @@ -6,6 +7,7 @@ priorities: List[gitbucket.core.model.Priority], defaultPriority: Option[gitbucket.core.model.Priority], labels: List[gitbucket.core.model.Label], + customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])], isManageable: Boolean, repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers @@ -147,6 +149,25 @@ @if(issue.isEmpty){ } + +@customFields.map { case (field, value) => +
+
+ @field.fieldName +
+ @gitbucket.core.model.CustomFieldBehavior(field.fieldType).map { behavior => + @if(issue.nonEmpty) { + @Html(behavior.fieldHtml(repository, issue.get.issueId, field.fieldId, value.map(_.value).getOrElse(""), isManageable)) + } + @if(issue.isEmpty) { + @Html(behavior.createHtml(repository, field.fieldId)) + } + } +
+
+ +} + @issue.map { issue => @gitbucket.core.plugin.PluginRegistry().getIssueSidebars.map { sidebarComponent => @sidebarComponent(issue, repository, context) @@ -167,7 +188,7 @@ $(function(){ @issue.map { issue => $('a.toggle-label').click(function(){ - var path = switchLabel($(this)); + const path = switchLabel($(this)); $.post('@helpers.url(repository)/issues/@issue.issueId/label/' + path, { labelId : $(this).data('label-id') }, function(data){ @@ -178,8 +199,8 @@ }); $('a.milestone').click(function(){ - var title = $(this).data('title'); - var milestoneId = $(this).data('id'); + const title = $(this).data('title'); + const milestoneId = $(this).data('id'); $.post('@helpers.url(repository)/issues/@issue.issueId/milestone', { milestoneId: milestoneId }, function(data){ @@ -189,11 +210,11 @@ }); $('a.priority').click(function(){ - var priorityName = $(this).data('name'); - var priorityId = $(this).data('id'); - var description = $(this).attr('title'); - var color = $(this).data('color'); - var fontColor = $(this).data('font-color'); + const priorityName = $(this).data('name'); + const priorityId = $(this).data('id'); + const description = $(this).attr('title'); + const color = $(this).data('color'); + const fontColor = $(this).data('font-color'); $.post('@helpers.url(repository)/issues/@issue.issueId/priority', { priorityId: priorityId }, function(data){ @@ -203,8 +224,8 @@ }); $('a.assign').click(function(){ - var $this = $(this); - var userName = $this.data('name'); + const $this = $(this); + const userName = $this.data('name'); $.post('@helpers.url(repository)/issues/@issue.issueId/assign', { assignedUserName: userName }, function(){ @@ -215,7 +236,7 @@ }.getOrElse { $('a.toggle-label').click(function(){ switchLabel($(this)); - var labelNames = Array(); + const labelNames = Array(); $('a.toggle-label').each(function(i, e){ if($(e).children('i').hasClass('octicon-check') == true){ labelNames.push($(e).text().trim()); @@ -232,32 +253,32 @@ }); $('a.milestone').click(function(){ - var title = $(this).data('title'); - var milestoneId = $(this).data('id'); + const title = $(this).data('title'); + const milestoneId = $(this).data('id'); displayMilestone(title, milestoneId); $('input[name=milestoneId]').val(milestoneId); }); $('a.priority').click(function(){ - var priorityName = $(this).data('name'); - var priorityId = $(this).data('id'); - var description = $(this).attr('title'); - var color = $(this).data('color'); - var fontColor = $(this).data('font-color'); + const priorityName = $(this).data('name'); + const priorityId = $(this).data('id'); + const description = $(this).attr('title'); + const color = $(this).data('color'); + const fontColor = $(this).data('font-color'); displayPriority(priorityName, priorityId, description, color, fontColor); $('input[name=priorityId]').val(priorityId); }); $('a.assign').click(function(){ - var $this = $(this); - var userName = $this.data('name'); + const $this = $(this); + const userName = $this.data('name'); displayAssignee($this, userName); $('input[name=assignedUserName]').val(userName); }); } function switchLabel($this){ - var i = $this.children('i'); + const i = $this.children('i'); if(i.hasClass('octicon-check')){ i.removeClass('octicon-check'); return 'delete'; diff --git a/src/main/twirl/gitbucket/core/pulls/compare.scala.html b/src/main/twirl/gitbucket/core/pulls/compare.scala.html index cd4a244..abcbfb0 100644 --- a/src/main/twirl/gitbucket/core/pulls/compare.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/compare.scala.html @@ -16,7 +16,8 @@ milestones: List[gitbucket.core.model.Milestone], priorities: List[gitbucket.core.model.Priority], defaultPriority: Option[gitbucket.core.model.Priority], - labels: List[gitbucket.core.model.Label])(implicit context: gitbucket.core.controller.Context) + labels: List[gitbucket.core.model.Label], + customFields: List[gitbucket.core.model.CustomField])(implicit context: gitbucket.core.controller.Context) @import gitbucket.core.view.helpers @gitbucket.core.html.main(s"Pull requests - ${repository.owner}/${repository.name}", Some(repository)){ @gitbucket.core.html.menu("pulls", repository){ @@ -101,7 +102,19 @@
- @gitbucket.core.issues.html.issueinfo(None, Nil, Nil, collaborators, milestones.map((_, 0, 0)), priorities, defaultPriority, labels, hasOriginWritePermission, repository) + @gitbucket.core.issues.html.issueinfo( + issue = None, + comments = Nil, + issueLabels = Nil, + collaborators = collaborators, + milestones = milestones.map((_, 0, 0)), + priorities = priorities, + defaultPriority = defaultPriority, + labels = labels, + customFields = customFields.map((_, None)), + isManageable = hasOriginWritePermission, + repository = repository + )
diff --git a/src/main/twirl/gitbucket/core/pulls/conversation.scala.html b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html index 9648526..c4b9f48 100644 --- a/src/main/twirl/gitbucket/core/pulls/conversation.scala.html +++ b/src/main/twirl/gitbucket/core/pulls/conversation.scala.html @@ -8,6 +8,7 @@ milestones: List[(gitbucket.core.model.Milestone, Int, Int)], priorities: List[gitbucket.core.model.Priority], labels: List[gitbucket.core.model.Label], + customFields: List[(gitbucket.core.model.CustomField, Option[gitbucket.core.model.IssueCustomField])], isEditable: Boolean, isManageable: Boolean, isManageableForkedRepository: Boolean, @@ -50,7 +51,19 @@ }
- @gitbucket.core.issues.html.issueinfo(Some(issue), comments.toList, issueLabels, collaborators, milestones, priorities, None, labels, isManageable, repository) + @gitbucket.core.issues.html.issueinfo( + Some(issue), + comments.toList, + issueLabels, + collaborators, + milestones, + priorities, + None, + labels, + customFields, + isManageable, + repository + )
diff --git a/src/main/twirl/gitbucket/core/settings/issuesfield.scala.html b/src/main/twirl/gitbucket/core/settings/issuesfield.scala.html new file mode 100644 index 0000000..1daf1f4 --- /dev/null +++ b/src/main/twirl/gitbucket/core/settings/issuesfield.scala.html @@ -0,0 +1,28 @@ +@(customField: gitbucket.core.model.CustomField) + + +
+
+ @customField.fieldName +
+
+ @customField.fieldType +
+
+ @if(customField.enableForIssues) { + Issues    + } + @if(customField.enableForPullRequests) { + Pull requests + } +
+
+
+ Edit +    + Delete +
+
+
+ + diff --git a/src/main/twirl/gitbucket/core/settings/issuesfieldform.scala.html b/src/main/twirl/gitbucket/core/settings/issuesfieldform.scala.html new file mode 100644 index 0000000..6824ad6 --- /dev/null +++ b/src/main/twirl/gitbucket/core/settings/issuesfieldform.scala.html @@ -0,0 +1,66 @@ +@(field: Option[gitbucket.core.model.CustomField], + repository: gitbucket.core.service.RepositoryService.RepositoryInfo)(implicit context: gitbucket.core.controller.Context) +@import gitbucket.core.view.helpers +@defining(field.map(_.fieldId).getOrElse("new")){ fieldId => +
+
+ + + + + + + + + +
+
+ +} diff --git a/src/main/twirl/gitbucket/core/settings/menu.scala.html b/src/main/twirl/gitbucket/core/settings/menu.scala.html index 2bcc10e..6e092cd 100644 --- a/src/main/twirl/gitbucket/core/settings/menu.scala.html +++ b/src/main/twirl/gitbucket/core/settings/menu.scala.html @@ -12,6 +12,9 @@ Branches } + + Issues + Service Hooks