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) =