Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So to date, everything has been an optional / minor breaking change. But changing constrained means that this will be a breaking change, no? @matrei what are your thoughts on this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdaugherty is adding a constraint breaking?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding the constraint isn't, but changing the base interface that's applied to every object I assume would be breaking. We need to test this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sbglasius is going to test this change with his side project to ensure it's backwards compatible.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we should test this, we don't want to have to recompile/re-release plugins for 7.1 compatibility.

/**
* @return Whether the value is editable
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@ class DefaultConstrainedProperty implements ConstrainedProperty {
protected final Map<String, Constraint> appliedConstraints = new LinkedHashMap<String, Constraint>()

// 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
*/
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The library being changed here doesn't have anything to do with scaffolding. Wouldn't it be better to introduce a constraint specific to scaffolding?

The more I think about this this seems very view-centric and it's being added to the domain layer. This just feels wrong from a separation perspective.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jdaugherty I am just expanding on the existing constraint which currently supports true/false and actually fixing it for when true is set. it can now be a boolean or an enum value.

*
* <p>This enum controls where a property is displayed in generated scaffolding views:</p>
* <ul>
* <li>{@link #ALL} - Display everywhere, including overriding default blacklists (e.g., dateCreated, lastUpdated)</li>
* <li>{@link #NONE} - Never display in any view</li>
* <li>{@link #INPUT_ONLY} - Display only in input forms (create/edit views)</li>
* <li>{@link #OUTPUT_ONLY} - Display only in output views (show/index views)</li>
* </ul>
*
* <p>Example usage in domain class constraints:</p>
* <pre>
* 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
* }
* }
* </pre>
*
* <p>For backwards compatibility, boolean values are also supported:</p>
* <ul>
* <li>{@code display: true} is equivalent to the default behavior (not setting display)</li>
* <li>{@code display: false} is equivalent to {@link #NONE}</li>
* </ul>
*
* @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

}
Original file line number Diff line number Diff line change
@@ -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
}
}
51 changes: 50 additions & 1 deletion grails-doc/src/en/ref/Constraints.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <textArea> tag.
|===

==== The display Constraint

The `display` constraint controls whether a property appears in scaffolded views. It accepts either a boolean value or a `DisplayType` enum value for fine-grained control.

===== Boolean Values (Backwards Compatible)

[source,groovy]
----
static constraints = {
internalNotes display: false // Never show this property
title display: true // Show this property (default behavior)
}
----

===== DisplayType Enum Values

For more control over where properties are displayed, use the `DisplayType` enum:

[source,groovy]
----
import static grails.gorm.validation.DisplayType.*

class Book {
String title
String isbn
Date dateCreated
Date lastUpdated
String internalNotes

static constraints = {
dateCreated display: ALL // Override blacklist, show in all views
lastUpdated display: OUTPUT_ONLY // Show only in show/index views
isbn display: INPUT_ONLY // Show only in create/edit forms
internalNotes display: NONE // Never show
}
}
----

|===
|DisplayType Value|Description

|`ALL`|Display the property in all views (input and output). This also overrides the default blacklist for properties like `dateCreated` and `lastUpdated`.
|`NONE`|Never display the property in any view. Equivalent to `display: false`.
|`INPUT_ONLY`|Display the property only in input views (create and edit forms).
|`OUTPUT_ONLY`|Display the property only in output views (show and index/list views).
|===

NOTE: Properties like `version`, `dateCreated`, and `lastUpdated` are excluded from input forms by default. Using `display: ALL` or `display: INPUT_ONLY` on these properties will override this blacklist and include them in input forms.


=== Programmatic access

Expand Down
Loading
Loading