From e68a21ee309e0a28014ca6644f53c1de9714219d Mon Sep 17 00:00:00 2001 From: Naoki Takezoe Date: Sun, 18 Dec 2022 22:46:11 +0900 Subject: [PATCH] Enum support in custom fields (#3195) --- .../resources/update/gitbucket-core_4.39.xml | 6 + .../gitbucket/core/GitBucketCoreModule.scala | 3 +- .../RepositorySettingsController.scala | 4 + .../gitbucket/core/model/CustomField.scala | 206 ++++++++++++++++-- .../core/service/CustomFieldsService.scala | 7 +- .../core/service/IssuesService.scala | 66 +++--- .../core/issues/issueinfo.scala.html | 8 +- .../core/settings/issuesfield.scala.html | 3 + .../core/settings/issuesfieldform.scala.html | 11 + 9 files changed, 257 insertions(+), 57 deletions(-) create mode 100644 src/main/resources/update/gitbucket-core_4.39.xml diff --git a/src/main/resources/update/gitbucket-core_4.39.xml b/src/main/resources/update/gitbucket-core_4.39.xml new file mode 100644 index 000000000..9691ce1b4 --- /dev/null +++ b/src/main/resources/update/gitbucket-core_4.39.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala index 08bdbf66b..74f71590c 100644 --- a/src/main/scala/gitbucket/core/GitBucketCoreModule.scala +++ b/src/main/scala/gitbucket/core/GitBucketCoreModule.scala @@ -115,5 +115,6 @@ object GitBucketCoreModule new Version("4.38.1"), new Version("4.38.2"), new Version("4.38.3"), - new Version("4.38.4") + new Version("4.38.4"), + new Version("4.39.0", new LiquibaseMigration("update/gitbucket-core_4.39.xml")), ) diff --git a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala index a949ba6c4..2a09fd100 100644 --- a/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala +++ b/src/main/scala/gitbucket/core/controller/RepositorySettingsController.scala @@ -126,6 +126,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { case class CustomFieldForm( fieldName: String, fieldType: String, + constraints: Option[String], enableForIssues: Boolean, enableForPullRequests: Boolean ) @@ -133,6 +134,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { val customFieldForm = mapping( "fieldName" -> trim(label("Field name", text(required, maxlength(100)))), "fieldType" -> trim(label("Field type", text(required))), + "constraints" -> trim(label("Constraints", optional(text()))), "enableForIssues" -> trim(label("Enable for issues", boolean(required))), "enableForPullRequests" -> trim(label("Enable for pull requests", boolean(required))), )(CustomFieldForm.apply) @@ -511,6 +513,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { repository.name, form.fieldName, form.fieldType, + if (form.fieldType == "enum") form.constraints else None, form.enableForIssues, form.enableForPullRequests ) @@ -533,6 +536,7 @@ trait RepositorySettingsControllerBase extends ControllerBase { params("fieldId").toInt, form.fieldName, form.fieldType, + if (form.fieldType == "enum") form.constraints else None, form.enableForIssues, form.enableForPullRequests ) diff --git a/src/main/scala/gitbucket/core/model/CustomField.scala b/src/main/scala/gitbucket/core/model/CustomField.scala index 4d5a4f1c5..0eaac53dc 100644 --- a/src/main/scala/gitbucket/core/model/CustomField.scala +++ b/src/main/scala/gitbucket/core/model/CustomField.scala @@ -5,6 +5,7 @@ import gitbucket.core.service.RepositoryService.RepositoryInfo import gitbucket.core.util.StringUtil import gitbucket.core.view.helpers import org.scalatra.i18n.Messages +import play.twirl.api.Html trait CustomFieldComponent extends TemplateComponent { self: Profile => import profile.api._ @@ -15,10 +16,11 @@ trait CustomFieldComponent extends TemplateComponent { self: Profile => val fieldId = column[Int]("FIELD_ID", O AutoInc) val fieldName = column[String]("FIELD_NAME") val fieldType = column[String]("FIELD_TYPE") + val constraints = column[Option[String]]("CONSTRAINTS") val enableForIssues = column[Boolean]("ENABLE_FOR_ISSUES") val enableForPullRequests = column[Boolean]("ENABLE_FOR_PULL_REQUESTS") def * = - (userName, repositoryName, fieldId, fieldName, fieldType, enableForIssues, enableForPullRequests) + (userName, repositoryName, fieldId, fieldName, fieldType, constraints, enableForIssues, enableForPullRequests) .<>(CustomField.tupled, CustomField.unapply) def byPrimaryKey(userName: String, repositoryName: String, fieldId: Int) = @@ -31,17 +33,28 @@ case class CustomField( repositoryName: String, fieldId: Int = 0, fieldName: String, - fieldType: String, // long, double, string, or date + fieldType: String, // long, double, string, date, or enum + constraints: Option[String], 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)( + def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])( implicit context: Context ): String - def validate(name: String, value: String, messages: Messages): Option[String] + def fieldHtml( + repository: RepositoryInfo, + issueId: Int, + fieldId: Int, + fieldName: String, + constraints: Option[String], + value: String, + editable: Boolean + )( + implicit context: Context + ): String + def validate(name: String, constraints: Option[String], value: String, messages: Messages): Option[String] } object CustomFieldBehavior { @@ -49,7 +62,7 @@ object CustomFieldBehavior { if (value.isEmpty) None else { CustomFieldBehavior(field.fieldType).flatMap { behavior => - behavior.validate(field.fieldName, value, messages) + behavior.validate(field.fieldName, field.constraints, value, messages) } } } @@ -60,12 +73,18 @@ object CustomFieldBehavior { case "double" => Some(DoubleFieldBehavior) case "string" => Some(StringFieldBehavior) case "date" => Some(DateFieldBehavior) + case "enum" => Some(EnumFieldBehavior) case _ => None } } case object LongFieldBehavior extends TextFieldBehavior { - override def validate(name: String, value: String, messages: Messages): Option[String] = { + override def validate( + name: String, + constraints: Option[String], + value: String, + messages: Messages + ): Option[String] = { try { value.toLong None @@ -75,7 +94,12 @@ object CustomFieldBehavior { } } case object DoubleFieldBehavior extends TextFieldBehavior { - override def validate(name: String, value: String, messages: Messages): Option[String] = { + override def validate( + name: String, + constraints: Option[String], + value: String, + messages: Messages + ): Option[String] = { try { value.toDouble None @@ -89,7 +113,12 @@ object CustomFieldBehavior { private val pattern = "yyyy-MM-dd" override protected val fieldType: String = "date" - override def validate(name: String, value: String, messages: Messages): Option[String] = { + override def validate( + name: String, + constraints: Option[String], + value: String, + messages: Messages + ): Option[String] = { try { new java.text.SimpleDateFormat(pattern).parse(value) None @@ -100,10 +129,142 @@ object CustomFieldBehavior { } } + case object EnumFieldBehavior extends CustomFieldBehavior { + override def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])( + implicit context: Context + ): String = { + createPulldownHtml(repository, fieldId, fieldName, constraints, None, None) + } + + override def fieldHtml( + repository: RepositoryInfo, + issueId: Int, + fieldId: Int, + fieldName: String, + constraints: Option[String], + value: String, + editable: Boolean + )(implicit context: Context): String = { + if (!editable) { + val sb = new StringBuilder + sb.append("""""") + sb.append("""
""") + if (value == "") { + sb.append(s"""No ${StringUtil.escapeHtml( + fieldName + )}""") + } else { + sb.append(s"""${StringUtil + .escapeHtml(value)}""") + } + sb.toString() + } else { + createPulldownHtml(repository, fieldId, fieldName, constraints, Some(issueId), Some(value)) + } + } + + private def createPulldownHtml( + repository: RepositoryInfo, + fieldId: Int, + fieldName: String, + constraints: Option[String], + issueId: Option[Int], + value: Option[String] + )(implicit context: Context): String = { + val sb = new StringBuilder + sb.append("""
""") + sb.append( + gitbucket.core.helper.html + .dropdown("Edit", right = true, filter = (fieldName, s"Filter $fieldName")) { + val options = new StringBuilder() + options.append( + s"""
  • Clear ${StringUtil + .escapeHtml(fieldName)}
  • """ + ) + constraints.foreach { + x => + x.split(",").map(_.trim).foreach { + item => + options.append(s"""
  • + | + | ${gitbucket.core.helper.html.checkicon(value.contains(item))} + | ${StringUtil.escapeHtml(item)} + | + |
  • + |""".stripMargin) + } + } + Html(options.toString()) + } + .toString() + ) + sb.append("""
    """) + sb.append("""
    """) + sb.append("""
    """) + value match { + case None => + sb.append(s"""No ${StringUtil.escapeHtml( + fieldName + )}""") + case Some(value) => + sb.append(s"""${StringUtil + .escapeHtml(value)}""") + } + if (value.isEmpty || issueId.isEmpty) { + sb.append(s"""""") + sb.append(s"""""".stripMargin) + } else { + sb.append(s""" + |""".stripMargin) + } + sb.toString() + } + + override def validate( + name: String, + constraints: Option[String], + value: String, + messages: Messages + ): Option[String] = None + } + trait TextFieldBehavior extends CustomFieldBehavior { protected val fieldType = "text" - def createHtml(repository: RepositoryInfo, fieldId: Int)(implicit context: Context): String = { + override def createHtml(repository: RepositoryInfo, fieldId: Int, fieldName: String, constraints: Option[String])( + implicit context: Context + ): String = { val sb = new StringBuilder sb.append( s"""""" @@ -111,8 +272,7 @@ object CustomFieldBehavior { sb.append(s""" }