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
+ }
+
+
+
+ |
+
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