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

ahuber pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/causeway.git


The following commit(s) were added to refs/heads/main by this push:
     new 2400f4189c2 CAUSEWAY-2297: fixes grid models for incubating viewers
2400f4189c2 is described below

commit 2400f4189c2802df46958f8c3e00158e408000f8
Author: Andi Huber <[email protected]>
AuthorDate: Thu Oct 23 13:34:18 2025 +0200

    CAUSEWAY-2297: fixes grid models for incubating viewers
---
 .../facets/object/grid/GridFacetDefault.java       |  51 ++++++++--
 .../grid/bootstrap/CollapseIfOneTabProcessor.java  |  52 +++++++++++
 .../grid/bootstrap/GridSystemServiceBootstrap.java |  98 +++++++++----------
 .../services/grid/bootstrap/_GridModel.java        |  57 +++++++----
 .../causeway/core/metamodel/util/Facets.java       |   5 +-
 .../applib/services/menu/model/MenuAction.java     |  41 +++++++-
 .../viewer/commons/model/layout/UiGridLayout.java  | 104 +++++++--------------
 7 files changed, 259 insertions(+), 149 deletions(-)

diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/GridFacetDefault.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/GridFacetDefault.java
index 4c2440a27e0..af3f01dc475 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/GridFacetDefault.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/GridFacetDefault.java
@@ -20,16 +20,21 @@
 
 import java.util.Map;
 import java.util.Optional;
+import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.BiConsumer;
 
 import org.jspecify.annotations.NonNull;
 import org.jspecify.annotations.Nullable;
 
+import org.apache.causeway.applib.layout.component.ActionLayoutData;
 import org.apache.causeway.applib.layout.grid.Grid;
+import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
 import org.apache.causeway.applib.services.grid.GridService;
+import org.apache.causeway.commons.internal.base._Casts;
 import org.apache.causeway.commons.internal.base._Lazy;
 import org.apache.causeway.commons.internal.base._Strings;
+import org.apache.causeway.commons.internal.collections._Sets;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
 import org.apache.causeway.core.metamodel.facetapi.Facet;
 import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
@@ -37,6 +42,8 @@
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 
 record GridFacetDefault(
     GridService gridService,
@@ -63,17 +70,21 @@ public static GridFacet create(
     @Override public FacetHolder getFacetHolder() { return facetHolder(); }
 
     @Override
-    public Grid getGrid(final @Nullable ManagedObject objectAdapter) {
-        guardAgainstObjectOfDifferentType(objectAdapter);
+    public Grid getGrid(final @Nullable ManagedObject mo) {
+        guardAgainstObjectOfDifferentType(mo);
 
         // gridByLayoutName is used as cache, unless 
gridService.supportsReloading() returns true
-        return gridByLayoutPrefix.compute(layoutPrefixFor(objectAdapter),
-                (layoutPrefix, cachedLayout)->
-                    (cachedLayout==null
-                            || gridService.supportsReloading())
-                    ? this.load(layoutPrefix)
-                    : cachedLayout
-        );
+        var grid = gridByLayoutPrefix.compute(layoutPrefixFor(mo),
+            (layoutPrefix, cachedLayout)->
+                (cachedLayout==null
+                        || gridService.supportsReloading())
+                ? this.load(layoutPrefix)
+                : cachedLayout);
+
+        _Casts.castTo(BSGrid.class, grid)
+            .ifPresent(bsGrid->attachAssociatedActions(bsGrid, mo));
+
+        return grid;
     }
 
     @Override
@@ -83,6 +94,28 @@ public void visitAttributes(final BiConsumer<String, Object> 
visitor) {
 
     // -- HELPER
 
+    private void attachAssociatedActions(BSGrid bsGrid, @Nullable 
ManagedObject mo) {
+        if(ManagedObjects.isNullOrUnspecifiedOrEmpty(mo)) return;
+
+        var primedActions = bsGrid.getAllActionsById();
+        final Set<String> actionIdsAlreadyAdded = 
_Sets.newHashSet(primedActions.keySet());
+
+        mo.objSpec().streamProperties(MixedIn.INCLUDED)
+        .forEach(property->{
+            Optional.ofNullable(
+                bsGrid.getAllPropertiesById().get(property.getId()))
+            .ifPresent(pl->{
+                ObjectAction.Util.findForAssociation(mo, property)
+                    .map(action->action.getId())
+                    .filter(id->!actionIdsAlreadyAdded.contains(id))
+                    .peek(actionIdsAlreadyAdded::add)
+                    .map(ActionLayoutData::new)
+                    .forEach(pl.getActions()::add);
+            });
+
+        });
+    }
+
     private void guardAgainstObjectOfDifferentType(final @Nullable 
ManagedObject objectAdapter) {
         if(ManagedObjects.isNullOrUnspecifiedOrEmpty(objectAdapter)) return; 
// cannot introspect
         if(!getSpecification().equals(objectAdapter.objSpec())) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/CollapseIfOneTabProcessor.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/CollapseIfOneTabProcessor.java
new file mode 100644
index 00000000000..50c9b84bdcd
--- /dev/null
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/CollapseIfOneTabProcessor.java
@@ -0,0 +1,52 @@
+/*
+ *  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.services.grid.bootstrap;
+
+import org.apache.causeway.applib.layout.grid.bootstrap.BSCol;
+import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
+import org.apache.causeway.applib.layout.grid.bootstrap.BSTabGroup;
+
+/**
+ * Conditionally collapses tab groups.
+ *
+ * <p> honors {@code <bs:tabGroup collapseIfOne="true">}
+ */
+record CollapseIfOneTabProcessor(BSGrid bsGrid) {
+
+    public void run() {
+        bsGrid.visit(new BSGrid.VisitorAdapter() {
+            @Override
+            public void visit(BSTabGroup bsTabGroup) {
+                if(bsTabGroup.isCollapseIfOne() == null
+                        || !bsTabGroup.isCollapseIfOne()
+                        || bsTabGroup.getTabs().size()>1) {
+                    return;
+                }
+                var parent = (BSCol) bsTabGroup.getOwner();
+                parent.getTabGroups().remove(bsTabGroup);
+                // relocate rows from tab to owning col
+                bsTabGroup.getTabs().get(0).getRows()
+                    .forEach(row->{
+                        parent.getRows().add(row);
+                        row.setOwner(parent);
+                    });
+            }
+        });
+    }
+}
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridSystemServiceBootstrap.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridSystemServiceBootstrap.java
index dcb5b126526..293c889efa0 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridSystemServiceBootstrap.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/GridSystemServiceBootstrap.java
@@ -20,6 +20,7 @@
 
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Set;
 import java.util.function.BiConsumer;
@@ -341,22 +342,20 @@ protected boolean validateAndNormalize(
             }
 
             if(!unboundPropertyIds.isEmpty()) {
-                var fieldSet = 
gridModel.getFieldSetForUnreferencedPropertiesRef();
-                if(fieldSet != null) {
-                    
unboundPropertyIds.removeAll(unboundMetadataContributingIds);
+                var fieldSet = gridModel.nodeForUnreferencedProperties();
+                unboundPropertyIds.removeAll(unboundMetadataContributingIds);
 
-                    // add unbound properties respecting configured sequence 
policy
-                    var sortedUnboundPropertyIds = _UnreferencedSequenceUtil
-                            .sortProperties(config, unboundPropertyIds.stream()
-                                    .map(oneToOneAssociationById::get)
-                                    .filter(_NullSafe::isPresent));
+                // add unbound properties respecting configured sequence policy
+                var sortedUnboundPropertyIds = _UnreferencedSequenceUtil
+                    .sortProperties(config, unboundPropertyIds.stream()
+                            .map(oneToOneAssociationById::get)
+                            .filter(_NullSafe::isPresent));
 
-                    addPropertiesTo(
-                            fieldSet,
-                            sortedUnboundPropertyIds,
-                            layoutDataFactory::createPropertyLayoutData,
-                            propertyLayoutDataById::put);
-                }
+                addPropertiesTo(
+                        fieldSet,
+                        sortedUnboundPropertyIds,
+                        layoutDataFactory::createPropertyLayoutData,
+                        propertyLayoutDataById::put);
             }
         }
 
@@ -378,23 +377,22 @@ protected boolean validateAndNormalize(
                             .map(oneToManyAssociationById::get)
                             .filter(_NullSafe::isPresent));
 
-            final BSTabGroup bsTabGroup = 
gridModel.getTabGroupForUnreferencedCollectionsRef();
-            if(bsTabGroup != null) {
-                addCollectionsTo(
-                        bsTabGroup,
-                        sortedMissingCollectionIds,
-                        objectSpec,
-                        layoutDataFactory::createCollectionLayoutData);
-            } else {
-                final BSCol bsCol = 
gridModel.getColForUnreferencedCollectionsRef();
-                if(bsCol != null) {
-                    addCollectionsTo(
+            gridModel.nodeForUnreferencedCollections()
+            .accept(
+                bsCol->{
+                    addUnreferencedCollectionsTo(
                         bsCol,
                         sortedMissingCollectionIds,
                         layoutDataFactory::createCollectionLayoutData,
                         collectionLayoutDataById::put);
-                }
-            }
+                },
+                bsTabGroup->{
+                    addUnreferencedCollectionsTo(
+                        bsTabGroup,
+                        sortedMissingCollectionIds,
+                        objectSpec,
+                        layoutDataFactory::createCollectionLayoutData);
+                });
         }
 
         // any missing actions will be added as actions in the specified column
@@ -502,25 +500,25 @@ protected boolean validateAndNormalize(
         }
 
         if(!missingActionIds.isEmpty()) {
-            final BSCol bsCol = gridModel.getColForUnreferencedActionsRef();
-            if(bsCol != null) {
-                addActionsTo(
-                        bsCol,
-                        missingActionIds,
-                        layoutDataFactory::createActionLayoutData,
-                        actionLayoutDataById::put);
-            } else {
-                final FieldSet fieldSet = 
gridModel.getFieldSetForUnreferencedActionsRef();
-                if(fieldSet != null) {
-                    addActionsTo(
+            gridModel.nodeForUnreferencedActions()
+                .accept(
+                    bsCol->{
+                        addActionsTo(
+                            bsCol,
+                            missingActionIds,
+                            layoutDataFactory::createActionLayoutData,
+                            actionLayoutDataById::put);
+                    },
+                    fieldSet->{
+                        addActionsTo(
                             fieldSet,
                             missingActionIds,
                             layoutDataFactory::createActionLayoutData,
                             actionLayoutDataById::put);
-                }
-            }
+                    });
         }
 
+        new CollapseIfOneTabProcessor(bsGrid).run();
         return true;
     }
 
@@ -546,7 +544,7 @@ private void addPropertiesTo(
         }
     }
 
-    private void addCollectionsTo(
+    private void addUnreferencedCollectionsTo(
             final BSCol tabRowCol,
             final Collection<String> collectionIds,
             final Function<String, CollectionLayoutData> layoutFactory,
@@ -559,21 +557,27 @@ private void addCollectionsTo(
         }
     }
 
-    private void addCollectionsTo(
+    private void addUnreferencedCollectionsTo(
             final BSTabGroup tabGroup,
             final Collection<String> collectionIds,
             final ObjectSpecification objectSpec,
             final Function<String, CollectionLayoutData> layoutFactory) {
 
-        for (final String collectionId : collectionIds) {
-            final BSTab bsTab = new BSTab();
+        // prevent multiple tabs with the same name
+        var tabsByName = new HashMap<String, BSTab>();
+        tabGroup.getTabs().forEach(tab->tabsByName.put(tab.getName(), tab));
 
+        for (final String collectionId : collectionIds) {
             var feature = objectSpec.getCollectionElseFail(collectionId);
             var featureCanonicalFriendlyName = 
feature.getCanonicalFriendlyName();
 
-            bsTab.setName(featureCanonicalFriendlyName);
-            tabGroup.getTabs().add(bsTab);
-            bsTab.setOwner(tabGroup);
+            final BSTab bsTab = 
tabsByName.computeIfAbsent(featureCanonicalFriendlyName, __->{
+                var newTab = new BSTab();
+                newTab.setName(featureCanonicalFriendlyName);
+                tabGroup.getTabs().add(newTab);
+                newTab.setOwner(tabGroup);
+                return newTab;
+            });
 
             final BSRow tabRow = new BSRow();
             tabRow.setOwner(bsTab);
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/_GridModel.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/_GridModel.java
index 582c9052f1f..71f0b099590 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/_GridModel.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/grid/bootstrap/_GridModel.java
@@ -21,6 +21,7 @@
 import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
+import java.util.Objects;
 import java.util.Optional;
 
 import org.apache.causeway.applib.layout.component.FieldSet;
@@ -28,12 +29,12 @@
 import org.apache.causeway.applib.layout.grid.bootstrap.BSGrid;
 import org.apache.causeway.applib.layout.grid.bootstrap.BSRow;
 import org.apache.causeway.applib.layout.grid.bootstrap.BSTabGroup;
+import org.apache.causeway.commons.functional.Either;
 import org.apache.causeway.commons.internal.collections._Maps;
 import org.apache.causeway.commons.internal.collections._Sets;
 import 
org.apache.causeway.core.metamodel.facets.members.layout.group.GroupIdAndName;
 
 import lombok.AccessLevel;
-import lombok.Getter;
 import lombok.NoArgsConstructor;
 
 /**
@@ -47,11 +48,32 @@ final class _GridModel {
         private final LinkedHashMap<String, BSCol> cols = 
_Maps.newLinkedHashMap();
         private final LinkedHashMap<String, FieldSet> fieldSets = 
_Maps.newLinkedHashMap();
 
-        @Getter private BSCol colForUnreferencedActionsRef;
-        @Getter private BSCol colForUnreferencedCollectionsRef;
-        @Getter private FieldSet fieldSetForUnreferencedActionsRef;
-        @Getter private FieldSet fieldSetForUnreferencedPropertiesRef;
-        @Getter private BSTabGroup tabGroupForUnreferencedCollectionsRef;
+        private BSCol colForUnreferencedActionsRef;
+        private FieldSet fieldSetForUnreferencedActionsRef;
+
+        private BSCol colForUnreferencedCollectionsRef;
+        private BSTabGroup tabGroupForUnreferencedCollectionsRef;
+
+        private FieldSet fieldSetForUnreferencedPropertiesRef;
+
+        // safe to call once validated
+        FieldSet nodeForUnreferencedProperties() {
+            return 
Objects.requireNonNull(fieldSetForUnreferencedPropertiesRef);
+        }
+
+        // safe to call once validated
+        Either<BSCol, FieldSet> nodeForUnreferencedActions() {
+            return colForUnreferencedActionsRef!=null
+                ? Either.left(colForUnreferencedActionsRef)
+                : Either.right(fieldSetForUnreferencedActionsRef);
+        }
+
+        // safe to call once validated
+        Either<BSCol, BSTabGroup> nodeForUnreferencedCollections() {
+            return colForUnreferencedCollectionsRef!=null
+                ? Either.left(colForUnreferencedCollectionsRef)
+                : Either.right(tabGroupForUnreferencedCollectionsRef);
+        }
 
         private boolean gridErrorsDetected = false;
 
@@ -190,25 +212,26 @@ public void visit(final BSTabGroup bsTabGroup) {
                 }
             });
 
-            if(gridModel.colForUnreferencedActionsRef == null && 
gridModel.fieldSetForUnreferencedActionsRef == null) {
+            boolean isValid = true;
+
+            if(gridModel.colForUnreferencedActionsRef == null
+                && gridModel.fieldSetForUnreferencedActionsRef == null) {
                 bsGrid.getMetadataErrors().add("No column and also no fieldset 
found with the 'unreferencedActions' attribute set");
+                isValid = false;
             }
             if(gridModel.fieldSetForUnreferencedPropertiesRef == null) {
                 bsGrid.getMetadataErrors().add("No fieldset found with the 
'unreferencedProperties' attribute set");
+                isValid = false;
             }
-            if(gridModel.colForUnreferencedCollectionsRef == null && 
gridModel.tabGroupForUnreferencedCollectionsRef == null) {
+            if(gridModel.colForUnreferencedCollectionsRef == null
+                && gridModel.tabGroupForUnreferencedCollectionsRef == null) {
                 bsGrid.getMetadataErrors().add("No column and also no tabgroup 
found with the 'unreferencedCollections' attribute set");
+                isValid = false;
             }
 
-            final boolean hasErrors =
-                    gridModel.colForUnreferencedActionsRef == null
-                    && gridModel.fieldSetForUnreferencedActionsRef == null
-                    || gridModel.fieldSetForUnreferencedPropertiesRef == null
-                    || gridModel.colForUnreferencedCollectionsRef == null
-                    && gridModel.tabGroupForUnreferencedCollectionsRef == null;
-
-            return hasErrors ? Optional.empty() : Optional.of(gridModel);
-
+            return isValid
+                ? Optional.of(gridModel)
+                : Optional.empty();
         }
 
         private void putRow(final String id, final BSRow bsRow) {
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java
index 10b800ad103..784f05524dd 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/Facets.java
@@ -24,6 +24,7 @@
 import java.util.stream.Stream;
 
 import org.jspecify.annotations.Nullable;
+
 import org.springframework.util.ClassUtils;
 
 import org.apache.causeway.applib.annotation.BookmarkPolicy;
@@ -121,9 +122,9 @@ public BookmarkPolicy 
bookmarkPolicyOrElseNotSpecified(final @Nullable FacetHold
     }
 
     public Optional<BSGrid> bootstrapGrid(
-            final ObjectSpecification objectSpec, final @Nullable 
ManagedObject objectAdapter) {
+            final ObjectSpecification objectSpec, final @Nullable 
ManagedObject mo) {
         return objectSpec.lookupFacet(GridFacet.class)
-            .map(gridFacet->gridFacet.getGrid(objectAdapter))
+            .map(gridFacet->gridFacet.getGrid(mo))
             .flatMap(grid->_Casts.castTo(BSGrid.class, grid));
     }
 
diff --git 
a/viewers/commons/applib/src/main/java/org/apache/causeway/viewer/commons/applib/services/menu/model/MenuAction.java
 
b/viewers/commons/applib/src/main/java/org/apache/causeway/viewer/commons/applib/services/menu/model/MenuAction.java
index 845baec005f..a98beadb512 100644
--- 
a/viewers/commons/applib/src/main/java/org/apache/causeway/viewer/commons/applib/services/menu/model/MenuAction.java
+++ 
b/viewers/commons/applib/src/main/java/org/apache/causeway/viewer/commons/applib/services/menu/model/MenuAction.java
@@ -20,30 +20,61 @@
 
 import java.util.Optional;
 
-import org.jspecify.annotations.Nullable;
+import org.jspecify.annotations.NonNull;
 
 import org.apache.causeway.applib.Identifier;
 import org.apache.causeway.applib.annotation.Where;
+import org.apache.causeway.applib.fa.FontAwesomeLayers;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.core.metamodel.consent.Consent.VetoReason;
 import org.apache.causeway.core.metamodel.context.MetaModelContext;
+import 
org.apache.causeway.core.metamodel.facets.members.iconfa.FaLayersProvider;
 import org.apache.causeway.core.metamodel.interactions.managed.ManagedAction;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.util.Facets;
 
-import org.jspecify.annotations.NonNull;
+import lombok.AccessLevel;
+import lombok.Builder;
 
 public record MenuAction (
         @NonNull Bookmark serviceBookmark,
         @NonNull Identifier actionId,
         @NonNull String name,
-        @Nullable String cssClassFa
+        @NonNull DecorationModel decorationModel
         ) implements MenuEntry {
 
+    @Builder(access = AccessLevel.PRIVATE)
+    public record DecorationModel(
+        boolean isPrototype,
+        int paramCount,
+        Optional<VetoReason> interactionVetoOpt,
+        Optional<FontAwesomeLayers> fontAwesomeLayersOpt,
+        Optional<String> describedAsOpt,
+        Optional<String> additionalCssClassOpt) {
+        static DecorationModel of(final @NonNull ManagedAction managedAction) {
+            var action = managedAction.getAction();
+            return DecorationModel.builder()
+                .isPrototype(action.isPrototype())
+                .paramCount(action.getParameterCount())
+                .interactionVetoOpt(managedAction.checkUsability()
+                    .flatMap(veto->veto.getReason()))
+                .fontAwesomeLayersOpt(ObjectAction.Util.cssClassFaFactoryFor(
+                    managedAction.getAction(),
+                    managedAction.getOwner())
+                    .map(FaLayersProvider::getLayers)
+                    .map(FontAwesomeLayers::emptyToBlank))
+                .describedAsOpt(managedAction.getDescription())
+                .additionalCssClassOpt(Facets.cssClass(action, 
managedAction.getOwner()))
+                .build();
+        }
+    }
+
     public static MenuAction of(final @NonNull ManagedAction managedAction) {
-        // TODO missing cssClass
         return new MenuAction(
                 managedAction.getOwner().getBookmark().orElseThrow(),
                 managedAction.getIdentifier(),
                 managedAction.getFriendlyName(),
-                null);
+                DecorationModel.of(managedAction));
     }
 
     public Optional<ManagedAction> managedAction(){
diff --git 
a/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/layout/UiGridLayout.java
 
b/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/layout/UiGridLayout.java
index 7591aa2ce25..1e9959533fe 100644
--- 
a/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/layout/UiGridLayout.java
+++ 
b/viewers/commons/model/src/main/java/org/apache/causeway/viewer/commons/model/layout/UiGridLayout.java
@@ -19,8 +19,6 @@
 package org.apache.causeway.viewer.commons.model.layout;
 
 import java.util.Optional;
-import java.util.Set;
-
 import org.apache.causeway.applib.layout.component.ActionLayoutData;
 import org.apache.causeway.applib.layout.component.CollectionLayoutData;
 import org.apache.causeway.applib.layout.component.DomainObjectLayoutData;
@@ -32,20 +30,16 @@
 import org.apache.causeway.applib.layout.grid.bootstrap.BSRow;
 import org.apache.causeway.applib.layout.grid.bootstrap.BSTab;
 import org.apache.causeway.applib.layout.grid.bootstrap.BSTabGroup;
-import org.apache.causeway.commons.internal.base._Lazy;
 import org.apache.causeway.commons.internal.base._NullSafe;
-import org.apache.causeway.commons.internal.collections._Sets;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
-import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
-import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
 import org.apache.causeway.core.metamodel.util.Facets;
 import org.apache.causeway.viewer.commons.model.UiModel;
 
-import org.jspecify.annotations.NonNull;
 import lombok.RequiredArgsConstructor;
 
-@RequiredArgsConstructor(staticName = "bind")
-public class UiGridLayout implements UiModel {
+public record UiGridLayout(
+    BSGrid bsGrid
+    ) implements UiModel {
 
     @RequiredArgsConstructor
     public static abstract class Visitor<C, T> {
@@ -61,53 +55,23 @@ public static abstract class Visitor<C, T> {
         protected abstract void onAction(C container, ActionLayoutData 
actionData);
         protected abstract void onProperty(C container, PropertyLayoutData 
propertyData);
         protected abstract void onCollection(C container, CollectionLayoutData 
collectionData);
-
     }
 
-    @NonNull private final ManagedObject managedObject;
-    private _Lazy<Optional<BSGrid>> gridData = 
_Lazy.threadSafe(this::initGridData);
-
-    public <C, T> void visit(final Visitor<C, T> visitor) {
-
-        // recursively visit the grid
-        gridData.get()
-        .ifPresent(bsGrid->{
-            for(var bsRow: bsGrid.getRows()) {
-                visitRow(bsRow, visitor.rootContainer, visitor);
-            }
-        });
-
+    public static Optional<UiGridLayout> createGrid(ManagedObject mo) {
+        return Facets.bootstrapGrid(mo.objSpec(), mo)
+            .map(UiGridLayout::new);
     }
 
-    private Optional<BSGrid> initGridData() {
-        return Facets.bootstrapGrid(managedObject.objSpec(), managedObject)
-        .map(this::attachAssociatedActions);
+    /**
+     * recursively visits the grid
+     */
+    public <C, T> void visit(final Visitor<C, T> visitor) {
+        for(var bsRow: bsGrid.getRows()) {
+            visitRow(bsRow, visitor.rootContainer, visitor);
+        }
     }
 
-    //TODO[refactor] this should not be necessary here, the GridFacet should 
already have done that for us
-    private BSGrid attachAssociatedActions(final BSGrid bSGrid) {
-
-        var primedActions = bSGrid.getAllActionsById();
-        final Set<String> actionIdsAlreadyAdded = 
_Sets.newHashSet(primedActions.keySet());
-
-        managedObject.objSpec().streamProperties(MixedIn.INCLUDED)
-        .forEach(property->{
-            Optional.ofNullable(
-                    bSGrid.getAllPropertiesById().get(property.getId()))
-            .ifPresent(pl->{
-
-                ObjectAction.Util.findForAssociation(managedObject, property)
-                .map(action->action.getId())
-                .filter(id->!actionIdsAlreadyAdded.contains(id))
-                .peek(actionIdsAlreadyAdded::add)
-                .map(ActionLayoutData::new)
-                .forEach(pl.getActions()::add);
-
-            });
-
-        });
-        return bSGrid;
-    }
+    // -- HELPER
 
     private <C, T> void visitRow(final BSRow bsRow, final C container, final 
Visitor<C, T> visitor) {
 
@@ -115,9 +79,7 @@ private <C, T> void visitRow(final BSRow bsRow, final C 
container, final Visitor
 
         for(var bsRowContent: bsRow.getCols()) {
             if(bsRowContent instanceof BSCol) {
-
                 visitCol((BSCol) bsRowContent, uiRow, visitor);
-
             } else if (bsRowContent instanceof BSClearFix) {
                 visitor.onClearfix(uiRow, (BSClearFix) bsRowContent);
             } else {
@@ -126,55 +88,59 @@ private <C, T> void visitRow(final BSRow bsRow, final C 
container, final Visitor
         }
     }
 
-    private <C, T> void visitCol(final BSCol bSCol, final C container, final 
Visitor<C, T> visitor) {
-        var uiCol = visitor.newCol(container, bSCol);
+    private <C, T> void visitCol(final BSCol bsCol, final C container, final 
Visitor<C, T> visitor) {
+
+        if(bsCol.getSpan() == 0) return; // skip
+
+        var uiCol = visitor.newCol(container, bsCol);
 
-        var hasDomainObject = bSCol.getDomainObject()!=null;
-        var hasActions = _NullSafe.size(bSCol.getActions())>0;
-        var hasRows = _NullSafe.size(bSCol.getRows())>0;
+        var hasDomainObject = bsCol.getDomainObject()!=null;
+        var hasActions = _NullSafe.size(bsCol.getActions())>0;
+        var hasRows = _NullSafe.size(bsCol.getRows())>0;
 
         if(hasDomainObject || hasActions) {
             var uiActionPanel = visitor.newActionPanel(uiCol);
             if(hasDomainObject) {
-                visitor.onObjectTitle(uiActionPanel, bSCol.getDomainObject());
+                visitor.onObjectTitle(uiActionPanel, bsCol.getDomainObject());
             }
             if(hasActions) {
-                for(var action : bSCol.getActions()) {
+                for(var action : bsCol.getActions()) {
                     visitor.onAction(uiActionPanel, action);
                 }
             }
         }
 
-        for(var fieldSet : bSCol.getFieldSets()) {
+        for(var fieldSet : bsCol.getFieldSets()) {
+            if(_NullSafe.isEmpty(fieldSet.getProperties())) continue; // skip 
empty fieldsets
             visitFieldSet(fieldSet, uiCol, visitor);
         }
 
-        for(var tabGroup : bSCol.getTabGroups()) {
+        for(var tabGroup : bsCol.getTabGroups()) {
             visitTabGroup(tabGroup, uiCol, visitor);
         }
 
         if(hasRows) {
-            for(var bsRow: bSCol.getRows()) {
+            for(var bsRow: bsCol.getRows()) {
                 visitRow(bsRow, uiCol, visitor);
             }
         }
 
-        for(var collectionData : bSCol.getCollections()) {
+        for(var collectionData : bsCol.getCollections()) {
             visitor.onCollection(uiCol, collectionData);
         }
 
     }
 
-    private <C, T> void visitTabGroup(final BSTabGroup BSColTabGroup, final C 
container, final Visitor<C, T> visitor) {
-        var uiTabGroup = visitor.newTabGroup(container, BSColTabGroup);
-        for(var bsTab: BSColTabGroup.getTabs()) {
+    private <C, T> void visitTabGroup(final BSTabGroup bsTabGroup, final C 
container, final Visitor<C, T> visitor) {
+        var uiTabGroup = visitor.newTabGroup(container, bsTabGroup);
+        for(var bsTab: bsTabGroup.getTabs()) {
             visitTab(bsTab, uiTabGroup, visitor);
         }
     }
 
-    private <C, T> void visitTab(final BSTab bSTab, final T container, final 
Visitor<C, T> visitor) {
-        var uiTab = visitor.newTab(container, bSTab);
-        for(var bsRow: bSTab.getRows()) {
+    private <C, T> void visitTab(final BSTab bsTab, final T container, final 
Visitor<C, T> visitor) {
+        var uiTab = visitor.newTab(container, bsTab);
+        for(var bsRow: bsTab.getRows()) {
             visitRow(bsRow, uiTab, visitor);
         }
     }

Reply via email to