diff --git a/form-builder/jvm/src/main/scala/org/orbeon/oxf/fb/AlertsAndConstraintsOps.scala b/form-builder/jvm/src/main/scala/org/orbeon/oxf/fb/AlertsAndConstraintsOps.scala
index 50c3c6f947..4cca5da9f3 100644
--- a/form-builder/jvm/src/main/scala/org/orbeon/oxf/fb/AlertsAndConstraintsOps.scala
+++ b/form-builder/jvm/src/main/scala/org/orbeon/oxf/fb/AlertsAndConstraintsOps.scala
@@ -392,6 +392,12 @@ trait AlertsAndConstraintsOps extends ControlOps {
object DatatypeValidation {
+ // `xs:string` is special since the empty string is still a valid `xs:string`, so `xs:string` is the same as
+ // `xf:string`. We shouldn't ever have to write `xf:string`. (XForms 2.0 even removes those `xf:*` types as
+ // they are not needed anymore, since `required` has precedence.) Here, we decide to return `xs:string` as the
+ // default datatype (`DefaultDataTypeValidation`), but we always mark it as not implying requiredness. The UI
+ // will show it as required only if the `required` validation implies it. However, for other `xs:*` types, we
+ // imply requiredness unless there is a `required` validation that implies otherwise.
private val DefaultDataTypeValidation =
DatatypeValidation(None, Left(XMLConstants.XS_STRING_QNAME -> false), None)
@@ -405,7 +411,7 @@ trait AlertsAndConstraintsOps extends ControlOps {
val isBuiltinType = Set(XF, XS)(qName.namespace.uri)
if (isBuiltinType)
- Left(qName -> (qName.namespace.uri == XS))
+ Left(qName -> (qName != XMLConstants.XS_STRING_QNAME && qName.namespace.uri == XS)) // see `DefaultDataTypeValidation`
else
Right(qName)
}
@@ -416,7 +422,7 @@ trait AlertsAndConstraintsOps extends ControlOps {
// https://github.com/orbeon/orbeon-forms/issues/6252
DatatypeValidation(
idOpt,
- value.trimAllToOpt.map(builtinOrSchemaType).getOrElse(DefaultDataTypeValidation.datatype),
+ value.trimAllToOpt.map(builtinOrSchemaType).getOrElse(DefaultDataTypeValidation.datatype), // see `DefaultDataTypeValidation`
alertOpt
)
} getOrElse
diff --git a/form-builder/jvm/src/test/scala/org/orbeon/oxf/fb/AlertsAndConstraintsTest.scala b/form-builder/jvm/src/test/scala/org/orbeon/oxf/fb/AlertsAndConstraintsTest.scala
index ab8b64d638..1ed18c0b97 100644
--- a/form-builder/jvm/src/test/scala/org/orbeon/oxf/fb/AlertsAndConstraintsTest.scala
+++ b/form-builder/jvm/src/test/scala/org/orbeon/oxf/fb/AlertsAndConstraintsTest.scala
@@ -17,6 +17,7 @@ import org.orbeon.dom.Document
import org.orbeon.oxf.fb.FormBuilder._
import org.orbeon.oxf.fr.FormRunner._
import org.orbeon.oxf.fr.SchemaOps.findSchemaPrefix
+import org.orbeon.oxf.fr.XMLNames.XF
import org.orbeon.oxf.test.{DocumentTestBase, ResourceManagerSupport}
import org.orbeon.oxf.xml.dom.Converter._
import org.orbeon.oxf.xml.{TransformerUtils, XMLConstants}
@@ -558,7 +559,48 @@ class AlertsAndConstraintsTest
}
}
- private def globalAlert (implicit ctx: FormBuilderDocContext) = AlertDetails(None, Nil, global = true)
+ describe("Writing `*:string` type with custom alert") {
+
+ for (required <- List(true, false))
+ it(s"must not write the `*:string` type for required = $required") {
+ withActionAndFBDoc(AlertsDoc) { implicit ctx =>
+
+ val newValidations = List(
+
+ {required}()
+
+ ,
+
+ string
+ {required}
+
+
+
+
+
+ )
+
+ writeAlertsAndValidationsAsXML(Control1, "", globalAlertAsXML, newValidations map elemToNodeInfo)
+
+ val xfTypeElem =
+ ctx.topLevelBindElem.get.descendant(XF -> "type").head
+
+ assert(xfTypeElem.id == "validation-2-validation")
+ assert(xfTypeElem.stringValue.isEmpty)
+
+ val expected =
+ DatatypeValidation(
+ Some("validation-2-validation"),
+ Left((XMLConstants.XS_STRING_QNAME, false)), // always `false` even for `xs:string`
+ Some(AlertDetails(Some("validation-2-validation"), List(("en", "Incorrect datatype"), ("fr", "")), global = false))
+ )
+
+ assert(expected == DatatypeValidation.fromForm(Control1))
+ }
+ }
+ }
+
+ private def globalAlert = AlertDetails(None, Nil, global = true)
private def globalAlertAsXML(implicit ctx: FormBuilderDocContext) = globalAlert.toXML
private def readConstraintValidationsAsXML(controlName: String)(implicit ctx: FormBuilderDocContext) =