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`.