diff --git a/grails-datamapping-validation/src/main/groovy/grails/gorm/validation/Constrained.groovy b/grails-datamapping-validation/src/main/groovy/grails/gorm/validation/Constrained.groovy index 50b7b48c7dc..9bf44c7a542 100644 --- a/grails-datamapping-validation/src/main/groovy/grails/gorm/validation/Constrained.groovy +++ b/grails-datamapping-validation/src/main/groovy/grails/gorm/validation/Constrained.groovy @@ -88,9 +88,16 @@ interface Constrained { */ boolean isUrl() /** - * @return Whether the value should be displayed + * @return Whether the value should be displayed (for backwards compatibility) + * @deprecated Use {@link #getDisplayType()} instead for more granular control */ boolean isDisplay() + + /** + * @return The display type controlling where this property is shown in scaffolded views + * @since 7.1 + */ + DisplayType getDisplayType() /** * @return Whether the value is editable */ diff --git a/grails-datamapping-validation/src/main/groovy/grails/gorm/validation/DefaultConstrainedProperty.groovy b/grails-datamapping-validation/src/main/groovy/grails/gorm/validation/DefaultConstrainedProperty.groovy index 56090084f68..8908205a9c6 100644 --- a/grails-datamapping-validation/src/main/groovy/grails/gorm/validation/DefaultConstrainedProperty.groovy +++ b/grails-datamapping-validation/src/main/groovy/grails/gorm/validation/DefaultConstrainedProperty.groovy @@ -70,8 +70,8 @@ class DefaultConstrainedProperty implements ConstrainedProperty { protected final Map appliedConstraints = new LinkedHashMap() // simple constraints - /** whether the property should be displayed */ - boolean display = true + /** the display type controlling where the property is shown in scaffolded views */ + DisplayType displayType = null /** * whether the property is editable */ @@ -581,6 +581,52 @@ class DefaultConstrainedProperty implements ConstrainedProperty { } } + /** + * @return Whether the property should be displayed (for backwards compatibility). + * Returns true unless displayType is explicitly set to NONE. + * @deprecated Use {@link #getDisplayType()} instead for more granular control + */ + @Override + boolean isDisplay() { + displayType != DisplayType.NONE + } + + /** + * Returns the display value for property access compatibility with ClassPropertyFetcher. + * Returns the displayType if set, otherwise returns the boolean display value. + * This method exists to ensure ClassPropertyFetcher.getPropertyDescriptor("display") works. + * @return The display value (DisplayType or Boolean) + */ + Object getDisplay() { + displayType != null ? displayType : isDisplay() + } + + /** + * Sets the display constraint with backwards compatibility for boolean values. + * @param value Can be a Boolean (true/false) or a DisplayType enum value + */ + void setDisplay(Object value) { + if (value instanceof Boolean) { + this.displayType = value ? null : DisplayType.NONE + } else if (value instanceof DisplayType) { + this.displayType = value + } else if (value == null) { + this.displayType = null + } else { + throw new IllegalArgumentException("display constraint must be a Boolean or DisplayType, got: ${value?.class?.name}") + } + } + + /** + * @return The display type controlling where this property is shown in scaffolded views. + * Returns null if not explicitly set (default behavior applies). + * @since 7.1 + */ + @Override + DisplayType getDisplayType() { + this.displayType + } + @SuppressWarnings('rawtypes') Map getAttributes() { return attributes diff --git a/grails-datamapping-validation/src/main/groovy/grails/gorm/validation/DisplayType.groovy b/grails-datamapping-validation/src/main/groovy/grails/gorm/validation/DisplayType.groovy new file mode 100644 index 00000000000..e7c5d90bc9e --- /dev/null +++ b/grails-datamapping-validation/src/main/groovy/grails/gorm/validation/DisplayType.groovy @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package grails.gorm.validation + +/** + * Enum representing the display behavior for a constrained property in scaffolded views. + * + *

This enum controls where a property is displayed in generated scaffolding views:

+ * + * + *

Example usage in domain class constraints:

+ *
+ * import static grails.gorm.validation.DisplayType.*
+ *
+ * class Book {
+ *     String title
+ *     Date dateCreated
+ *     String internalNotes
+ *
+ *     static constraints = {
+ *         dateCreated display: ALL      // Override blacklist, show everywhere
+ *         internalNotes display: NONE   // Never show
+ *     }
+ * }
+ * 
+ * + *

For backwards compatibility, boolean values are also supported:

+ * + * + * @author Scott Murphy Heiberg + * @since 7.1 + */ +enum DisplayType { + + /** + * Display the property in all views (input and output). + * This also overrides the default blacklist for properties like dateCreated and lastUpdated. + */ + ALL, + + /** + * Never display the property in any view. + */ + NONE, + + /** + * Display the property only in input views (create and edit forms). + */ + INPUT_ONLY, + + /** + * Display the property only in output views (show and index/list views). + */ + OUTPUT_ONLY + +} diff --git a/grails-datamapping-validation/src/test/groovy/grails/gorm/validation/DisplayTypeSpec.groovy b/grails-datamapping-validation/src/test/groovy/grails/gorm/validation/DisplayTypeSpec.groovy new file mode 100644 index 00000000000..9f76eeb6819 --- /dev/null +++ b/grails-datamapping-validation/src/test/groovy/grails/gorm/validation/DisplayTypeSpec.groovy @@ -0,0 +1,151 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package grails.gorm.validation + +import org.grails.datastore.gorm.validation.constraints.registry.DefaultConstraintRegistry +import spock.lang.Specification + +class DisplayTypeSpec extends Specification { + + DefaultConstrainedProperty constrainedProperty + + void setup() { + constrainedProperty = new DefaultConstrainedProperty( + TestDomain, + "testProperty", + String, + new DefaultConstraintRegistry() + ) + } + + void "test default displayType is null"() { + expect: + constrainedProperty.displayType == null + } + + void "test default isDisplay returns true"() { + expect: + constrainedProperty.display == true + } + + void "test setDisplay with boolean false sets displayType to NONE"() { + when: + constrainedProperty.setDisplay(false) + + then: + constrainedProperty.displayType == DisplayType.NONE + constrainedProperty.display == false + } + + void "test setDisplay with boolean true sets displayType to null"() { + when: + constrainedProperty.setDisplay(true) + + then: + constrainedProperty.displayType == null + constrainedProperty.display == true + } + + void "test setDisplay with DisplayType.ALL"() { + when: + constrainedProperty.setDisplay(DisplayType.ALL) + + then: + constrainedProperty.displayType == DisplayType.ALL + constrainedProperty.display == true + } + + void "test setDisplay with DisplayType.NONE"() { + when: + constrainedProperty.setDisplay(DisplayType.NONE) + + then: + constrainedProperty.displayType == DisplayType.NONE + constrainedProperty.display == false + } + + void "test setDisplay with DisplayType.INPUT_ONLY"() { + when: + constrainedProperty.setDisplay(DisplayType.INPUT_ONLY) + + then: + constrainedProperty.displayType == DisplayType.INPUT_ONLY + constrainedProperty.display == true // isDisplay still returns true for INPUT_ONLY + } + + void "test setDisplay with DisplayType.OUTPUT_ONLY"() { + when: + constrainedProperty.setDisplay(DisplayType.OUTPUT_ONLY) + + then: + constrainedProperty.displayType == DisplayType.OUTPUT_ONLY + constrainedProperty.display == true // isDisplay still returns true for OUTPUT_ONLY + } + + void "test setDisplay with null resets to default"() { + given: + constrainedProperty.setDisplay(DisplayType.NONE) + + when: + constrainedProperty.setDisplay(null) + + then: + constrainedProperty.displayType == null + constrainedProperty.display == true + } + + void "test setDisplay with invalid type throws exception"() { + when: + constrainedProperty.setDisplay("invalid") + + then: + thrown(IllegalArgumentException) + } + + void "test applyConstraint with display false sets displayType to NONE"() { + when: + constrainedProperty.applyConstraint("display", false) + + then: + constrainedProperty.displayType == DisplayType.NONE + constrainedProperty.display == false + } + + void "test applyConstraint with display true sets displayType to null"() { + when: + constrainedProperty.applyConstraint("display", true) + + then: + constrainedProperty.displayType == null + constrainedProperty.display == true + } + + void "test applyConstraint with DisplayType enum"() { + when: + constrainedProperty.applyConstraint("display", DisplayType.INPUT_ONLY) + + then: + constrainedProperty.displayType == DisplayType.INPUT_ONLY + constrainedProperty.display == true + } + + static class TestDomain { + String testProperty + } +} diff --git a/grails-doc/src/en/ref/Constraints.adoc b/grails-doc/src/en/ref/Constraints.adoc index 405d1653c9a..26dc2d74e90 100644 --- a/grails-doc/src/en/ref/Constraints.adoc +++ b/grails-doc/src/en/ref/Constraints.adoc @@ -176,13 +176,62 @@ Some constraints have no impact on persistence but customize the scaffolding. It |=== |Constraint|Description -|display|Boolean that determines whether the property is displayed in the scaffolding views. If `true` (the default), the property _is_ displayed. +|display|Controls whether and where the property is displayed in scaffolding views. Accepts a boolean or a `DisplayType` enum value (see below). |editable|Boolean that determines whether the property can be edited from the scaffolding views. If `false`, the associated form fields are displayed in read-only mode. |format|Specify a display format for types that accept one, such as dates. For example, 'yyyy-MM-dd'. |password|Boolean indicating whether this is property should be displayed with a password field. Only works on fields that would normally be displayed with a text field. |widget|Controls what widget is used to display the property. For example, 'textarea' will force the scaffolding to use a