You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/06/07 14:49:02 UTC

(causeway) 11/50: CAUSEWAY-4012: disables empty selector actions

This is an automated email from the ASF dual-hosted git repository.

danhaywood pushed a commit to branch CAUSEWAY-4014
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit cb053d525dab32cc93dcd195e90071a388381e45
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Sun Jun 7 06:55:11 2026 +0100

    CAUSEWAY-4012: disables empty selector actions
---
 ...ledFacetForEmptyParentedCollectionSelector.java | 66 ++++++++++++++++++++++
 .../specimpl/ObjectSpecificationAbstract.java      |  2 +
 .../ParentedCollectionSelectorActionUtilTest.java  | 21 +++++++
 .../disable-empty-selector-actions/tasks.md        | 16 +++---
 4 files changed, 97 insertions(+), 8 deletions(-)

diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/synthetic/DisabledFacetForEmptyParentedCollectionSelector.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/synthetic/DisabledFacetForEmptyParentedCollectionSelector.java
new file mode 100644
index 00000000000..da8aba5244e
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/synthetic/DisabledFacetForEmptyParentedCollectionSelector.java
@@ -0,0 +1,66 @@
+/*
+ *  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
+ *
+ *        http://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 org.apache.causeway.core.metamodel.facets.actions.synthetic;
+
+import java.util.Optional;
+
+import org.apache.causeway.applib.annotation.Where;
+import org.apache.causeway.core.metamodel.consent.Consent.VetoReason;
+import org.apache.causeway.core.metamodel.facets.collections.CollectionFacet;
+import org.apache.causeway.core.metamodel.facets.members.disabled.DisabledFacetAbstract;
+import org.apache.causeway.core.metamodel.interactions.UsabilityContext;
+import org.apache.causeway.core.metamodel.object.ManagedObjects;
+import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
+
+import lombok.NonNull;
+import lombok.val;
+
+/**
+ * Disables a synthetic selector action when its associated collection has no selectable rows.
+ */
+public class DisabledFacetForEmptyParentedCollectionSelector
+extends DisabledFacetAbstract {
+
+    public static final String REASON = "No items to select.";
+
+    private final @NonNull OneToManyAssociation collection;
+
+    public DisabledFacetForEmptyParentedCollectionSelector(
+            final @NonNull OneToManyAssociation collection,
+            final @NonNull org.apache.causeway.core.metamodel.facetapi.FacetHolder holder) {
+        super(Where.ANYWHERE, VetoReason.explicit(REASON), holder);
+        this.collection = collection;
+    }
+
+    @Override
+    public Optional<VetoReason> disables(final UsabilityContext ic) {
+        val target = ic.getTarget();
+        if(ManagedObjects.isNullOrUnspecifiedOrEmpty(target)) {
+            return Optional.empty();
+        }
+        val collectionAdapter = collection.get(target, ic.getInitiatedBy());
+        val hasItems = CollectionFacet.streamAdapters(collectionAdapter)
+                .findAny()
+                .isPresent();
+        return hasItems
+                ? Optional.empty()
+                : Optional.of(VetoReason.explicit(REASON));
+    }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
index 94d811d9a53..b6f6bf4dbfd 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
@@ -35,6 +35,7 @@
 import org.apache.causeway.core.metamodel.facets.actions.synthetic.ActionSemanticsFacetForParentedCollectionSelector;
 import org.apache.causeway.core.metamodel.facets.actions.synthetic.ActionValidationFacetForParentedCollectionSelector;
 import org.apache.causeway.core.metamodel.facets.actions.synthetic.CssClassFacetForParentedCollectionSelector;
+import org.apache.causeway.core.metamodel.facets.actions.synthetic.DisabledFacetForEmptyParentedCollectionSelector;
 import org.apache.causeway.core.metamodel.facets.actions.synthetic.DisabledFacetForParentedCollectionSelectorParent;
 import org.apache.causeway.core.metamodel.facets.actions.synthetic.FaFacetForParentedCollectionSelector;
 import org.apache.causeway.core.metamodel.facets.actions.synthetic.LayoutGroupFacetForParentedCollectionSelector;
@@ -1170,6 +1171,7 @@ private static void installActionFacets(
             FacetUtil.addFacet(new LayoutGroupFacetForParentedCollectionSelector(
                     collection.getId(), collection.getCanonicalFriendlyName(), facetedMethod));
             FacetUtil.addFacet(new ParentedCollectionSelectorFacetDefault(collection, facetedMethod));
+            FacetUtil.addFacet(new DisabledFacetForEmptyParentedCollectionSelector(collection, facetedMethod));
             FacetUtil.addFacet(new ActionSemanticsFacetForParentedCollectionSelector(facetedMethod));
             FacetUtil.addFacet(new ActionValidationFacetForParentedCollectionSelector(
                     collection,
diff --git a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/specloader/specimpl/ParentedCollectionSelectorActionUtilTest.java b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/specloader/specimpl/ParentedCollectionSelectorActionUtilTest.java
index 535da9b47a2..5464211f9be 100644
--- a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/specloader/specimpl/ParentedCollectionSelectorActionUtilTest.java
+++ b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/specloader/specimpl/ParentedCollectionSelectorActionUtilTest.java
@@ -417,6 +417,27 @@ void disables_parent_parameter_but_not_scalar_filter_parameters() {
                 InteractionInitiatedBy.USER).isAllowed(), is(true));
     }
 
+    @Test
+    void disables_selector_action_when_associated_collection_is_empty() {
+        val lease = new Lease();
+        val leaseAdapter = mmc.getObjectManager().adapt(lease);
+
+        val consent = selectorAction.isUsable(leaseAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE);
+
+        assertThat(selectorAction.getFacet(DisabledFacet.class), instanceOf(DisabledFacetForEmptyParentedCollectionSelector.class));
+        assertThat(consent.isVetoed(), is(true));
+        assertThat(consent.getReasonAsString().orElseThrow(), containsString(DisabledFacetForEmptyParentedCollectionSelector.REASON));
+    }
+
+    @Test
+    void leaves_selector_action_enabled_when_associated_collection_is_not_empty() {
+        val lease = new Lease();
+        lease.getItems().add(new LeaseItem("first", 1, null));
+        val leaseAdapter = mmc.getObjectManager().adapt(lease);
+
+        assertThat(selectorAction.isUsable(leaseAdapter, InteractionInitiatedBy.USER, Where.ANYWHERE).isAllowed(), is(true));
+    }
+
     @Test
     void validates_and_invokes_selector_action_returning_single_matching_child() {
         val lease = new Lease();
diff --git a/openspec/changes/disable-empty-selector-actions/tasks.md b/openspec/changes/disable-empty-selector-actions/tasks.md
index 41956d0ecfd..e0c581aeafb 100644
--- a/openspec/changes/disable-empty-selector-actions/tasks.md
+++ b/openspec/changes/disable-empty-selector-actions/tasks.md
@@ -1,16 +1,16 @@
 ## 1. Action Disabled Facet
 
-- [ ] 1.1 Add an action-level disabled facet for synthetic parented collection selector actions.
-- [ ] 1.2 Implement the disabled facet so it reads the associated collection from the current action target and vetoes usability when the collection is empty.
-- [ ] 1.3 Install the disabled facet when `ParentedCollectionSelectorActionUtil` creates a synthetic selector action.
+- [x] 1.1 Add an action-level disabled facet for synthetic parented collection selector actions.
+- [x] 1.2 Implement the disabled facet so it reads the associated collection from the current action target and vetoes usability when the collection is empty.
+- [x] 1.3 Install the disabled facet when `ParentedCollectionSelectorActionUtil` creates a synthetic selector action.
 
 ## 2. Tests
 
-- [ ] 2.1 Add metamodel test coverage that an empty associated collection disables the selector action with a clear reason.
-- [ ] 2.2 Add metamodel test coverage that a non-empty associated collection leaves the selector action enabled.
-- [ ] 2.3 Verify existing no-match validation behavior remains unchanged for non-empty collections.
+- [x] 2.1 Add metamodel test coverage that an empty associated collection disables the selector action with a clear reason.
+- [x] 2.2 Add metamodel test coverage that a non-empty associated collection leaves the selector action enabled.
+- [x] 2.3 Verify existing no-match validation behavior remains unchanged for non-empty collections.
 
 ## 3. Validation
 
-- [ ] 3.1 Run the focused `ParentedCollectionSelectorActionUtilTest` Maven test set.
-- [ ] 3.2 Run `openspec validate disable-empty-selector-actions --strict`.
+- [x] 3.1 Run the focused `ParentedCollectionSelectorActionUtilTest` Maven test set.
+- [x] 3.2 Run `openspec validate disable-empty-selector-actions --strict`.