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

ahuber pushed a commit to branch 3971-layout.switching
in repository https://gitbox.apache.org/repos/asf/causeway.git

commit 39b66b1e48ef76cb7565cb974552e027ca176f02
Author: andi-huber <[email protected]>
AuthorDate: Tue Feb 24 08:14:49 2026 +0100

    CAUSEWAY-3971: adds reproducer test
    
    Task-Url: https://issues.apache.org/jira/browse/CAUSEWAY-3971
---
 .../applib/layout/grid/bootstrap/BSUtil.java       | 121 +++++++++++++++++++++
 .../metamodel/facets/object/grid/BSGridFacet.java  |   9 +-
 .../metamodel/services/grid/Bar-simple.layout.xml  |  37 +++++++
 .../causeway/core/metamodel/services/grid/Bar.java |  33 ++++++
 .../services/grid/LayoutSwitchingTest.java         |  65 +++++++++++
 .../components/layout/bs/BSGridPanelFactory.java   |   5 +-
 6 files changed, 262 insertions(+), 8 deletions(-)

diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java
index 9d207ef9da7..9f7782757b4 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/layout/grid/bootstrap/BSUtil.java
@@ -32,6 +32,7 @@
 import org.apache.causeway.applib.layout.component.FieldSet;
 import org.apache.causeway.applib.layout.component.PropertyLayoutData;
 import 
org.apache.causeway.applib.layout.grid.bootstrap.BSElement.BSElementVisitor;
+
 import lombok.experimental.UtilityClass;
 
 @UtilityClass
@@ -83,6 +84,126 @@ public BSGrid resolveOwners(final BSGrid grid) {
         return grid;
     }
 
+    /**
+     * Useful for debugging or comparing grid instances (e.g. JUnit tests).
+     */
+    public String toYaml(final BSGrid grid) {
+        class TinyWriter {
+            StringBuilder sb = new StringBuilder();
+            int indent = 0;
+            void inc() { indent++; }
+            void dec() { indent--; }
+            void writeln(final String line) {
+                sb.append("  ".repeat(indent)).append(line).append("\n");
+            }
+        }
+        var w = new TinyWriter();
+
+        grid.visit(new BSElementVisitor() {
+            @Override public void enter(final BSGrid bsGrid) {
+                w.writeln("bsGrid:");
+                w.inc();
+                w.writeln("class: 
%s".formatted(bsGrid.domainClass().getName()));
+            }
+            @Override public void exit(final BSGrid bsGrid) {
+                w.dec();
+            }
+            @Override public void enter(final BSRow bsRow) {
+                w.writeln("row:");
+                w.inc();
+            }
+            @Override public void exit(final BSRow bsRow) {
+                w.dec();
+            }
+            @Override public void enter(final BSCol bsCol) {
+                w.writeln("col:");
+                w.inc();
+                w.writeln("span: %s".formatted(bsCol.getSpan()));
+                if(bsCol.isUnreferencedActions()) {
+                    w.writeln("unreferencedActions: true");
+                }
+                if(bsCol.isUnreferencedCollections()) {
+                    w.writeln("unreferencedCollections: true");
+                }
+            }
+            @Override public void exit(final BSCol bsCol) {
+                w.dec();
+            }
+            @Override public void enter(final BSTabGroup bsTabGroup) {
+                w.writeln("tabGroup:");
+                w.inc();
+            }
+            @Override public void exit(final BSTabGroup bsTabGroup) {
+                w.dec();
+            }
+            @Override public void enter(final BSTab bsTab) {
+                w.writeln("tab:");
+                w.inc();
+                w.writeln("name: %s".formatted(bsTab.getName()));
+            }
+            @Override public void exit(final BSTab bsTab) {
+                w.dec();
+            }
+
+            @Override public void visit(final BSClearFix bsClearFix) {}
+            @Override public void visit(final DomainObjectLayoutData 
domainObjectLayoutData) {
+                w.inc();
+                w.writeln("domainObject:");
+                w.inc();
+                w.writeln("named: 
%s".formatted(domainObjectLayoutData.getNamed()));
+                w.writeln("bookmarking: 
%s".formatted(domainObjectLayoutData.getBookmarking()));
+                w.dec();
+                w.dec();
+            }
+            @Override public void visit(final ActionLayoutData 
actionLayoutData) {
+                w.inc();
+                w.writeln("action:");
+                w.inc();
+                w.writeln("id: %s".formatted(actionLayoutData.getId()));
+                w.writeln("named: %s".formatted(actionLayoutData.getNamed()));
+                w.writeln("position: 
%s".formatted(actionLayoutData.getPosition()));
+                w.writeln("promptStyle: 
%s".formatted(actionLayoutData.getPromptStyle()));
+                w.writeln("hidden: 
%s".formatted(actionLayoutData.getHidden()));
+                w.dec();
+                w.dec();
+            }
+            @Override public void visit(final PropertyLayoutData 
propertyLayoutData) {
+                w.inc();
+                w.writeln("property:");
+                w.inc();
+                w.writeln("id: %s".formatted(propertyLayoutData.getId()));
+                w.writeln("named: 
%s".formatted(propertyLayoutData.getNamed()));
+                w.writeln("hidden: 
%s".formatted(propertyLayoutData.getHidden()));
+                w.dec();
+                w.dec();
+            }
+            @Override public void visit(final CollectionLayoutData 
collectionLayoutData) {
+                w.inc();
+                w.writeln("collection:");
+                w.inc();
+                w.writeln("id: %s".formatted(collectionLayoutData.getId()));
+                w.writeln("named: 
%s".formatted(collectionLayoutData.getNamed()));
+                w.writeln("hidden: 
%s".formatted(collectionLayoutData.getHidden()));
+                w.dec();
+                w.dec();
+            }
+            @Override public void visit(final FieldSet fieldSet) {
+                w.writeln("fieldSet:");
+                w.inc();
+                w.writeln("id: %s".formatted(fieldSet.getId()));
+                w.writeln("name: %s".formatted(fieldSet.getName()));
+                if(fieldSet.isUnreferencedActions()) {
+                    w.writeln("unreferencedActions: true");
+                }
+                if(fieldSet.isUnreferencedProperties()) {
+                    w.writeln("unreferencedProperties: true");
+                }
+                w.dec();
+            }
+        });
+        return w.sb.toString();
+    }
+
     // -- REMOVERS
 
     /** removes the tab from its owner and returns the owner */
diff --git 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/BSGridFacet.java
 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/BSGridFacet.java
index 84866b2486c..e37944dbc06 100644
--- 
a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/BSGridFacet.java
+++ 
b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/object/grid/BSGridFacet.java
@@ -62,6 +62,9 @@ public static GridFacet create(
 
     @Override
     public BSGrid getGrid(final @Nullable ManagedObject mo) {
+        if(mo!=null) {
+            System.err.println("get grid for %s: 
%s".formatted(layoutPrefixFor(mo), mo));
+        }
         guardAgainstObjectOfDifferentType(mo);
         return normalized(mo);
     }
@@ -87,21 +90,19 @@ private BSGrid normalized(final @Nullable ManagedObject mo) 
{
 
     private void guardAgainstObjectOfDifferentType(final @Nullable 
ManagedObject objectAdapter) {
         if(ManagedObjects.isNullOrUnspecifiedOrEmpty(objectAdapter)) return; 
// cannot introspect
-        if(!objSpec().equals(objectAdapter.objSpec())) {
+        if(!objSpec().equals(objectAdapter.objSpec()))
             throw _Exceptions.unrecoverable(
                     "getGrid(adapter) was called passing an adapter (type: 
%s), "
                     + "for which this GridFacet (type: %s) is not responsible; 
"
                     + "indicates that some framework internals are wired up in 
a wrong way",
                     objectAdapter.objSpec().getCorrespondingClass().getName(),
                     objSpec().getCorrespondingClass().getName());
-        }
     }
 
     private String layoutPrefixFor(final @Nullable ManagedObject 
objectAdapter) {
         if(ManagedObjects.isNullOrUnspecifiedOrEmpty(objectAdapter)
-            || !hasLayoutPrefixFacet()) {
+            || !hasLayoutPrefixFacet())
             return "";
-        }
         var layoutName = 
_Strings.nullToEmpty(layoutFacetLazy.get().layoutPrefix(objectAdapter));
         return layoutName;
     }
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/Bar-simple.layout.xml
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/Bar-simple.layout.xml
new file mode 100644
index 00000000000..ee709cc628e
--- /dev/null
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/Bar-simple.layout.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  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.
+-->
+<bs:grid 
xsi:schemaLocation="https://causeway.apache.org/applib/layout/component 
https://causeway.apache.org/applib/layout/component/component.xsd 
https://causeway.apache.org/applib/layout/links 
https://causeway.apache.org/applib/layout/links/links.xsd 
https://causeway.apache.org/applib/layout/grid/bootstrap3 
https://causeway.apache.org/applib/layout/grid/bootstrap3/bootstrap3.xsd"; 
xmlns:bs="https://causeway.apache.org/applib/layout/grid/bootstrap3"; 
xmlns:cpt="https://causeway.apache.org [...]
+    <bs:row>
+        <bs:col span="12" unreferencedActions="true">
+            <cpt:domainObject/>
+            <cpt:action id="createSimpleObject" hidden="EVERYWHERE"/>
+        </bs:col>
+    </bs:row>
+    <bs:row>
+        <bs:col span="4">
+            <cpt:fieldSet name="General" unreferencedProperties="true">
+                <cpt:property id="name" hidden="EVERYWHERE"/>
+            </cpt:fieldSet>
+        </bs:col>
+        <bs:col span="12" unreferencedCollections="true">
+            <cpt:collection id="sampleCollection" hidden="EVERYWHERE"/>
+        </bs:col>
+    </bs:row>
+</bs:grid>
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/Bar.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/Bar.java
index 881913d1812..27b8617046a 100644
--- 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/Bar.java
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/Bar.java
@@ -18,20 +18,53 @@
  */
 package org.apache.causeway.core.metamodel.services.grid;
 
+import java.util.List;
+
 import jakarta.inject.Named;
 
 import org.apache.causeway.applib.annotation.Action;
+import org.apache.causeway.applib.annotation.Collection;
 import org.apache.causeway.applib.annotation.DomainObject;
 import org.apache.causeway.applib.annotation.Nature;
+import org.apache.causeway.applib.annotation.ObjectSupport;
+import org.apache.causeway.applib.annotation.Property;
+
+import lombok.RequiredArgsConstructor;
 
 @Named("simple.Bar")
 @DomainObject(
         nature = Nature.VIEW_MODEL)
+@RequiredArgsConstructor
 public class Bar {
 
+    private final String name;
+
+    public Bar() {
+        this.name = "Bar";
+    }
+
+    @ObjectSupport
+    public String layout() {
+        return name.toLowerCase().contains("simple")
+            ? "simple"
+            : name.toLowerCase().contains("full")
+                ? "full"
+                : null;
+    }
+
     @Action
     public Object createSimpleObject() {
         return new Object();
     }
 
+    @Property
+    public String getName() {
+        return name;
+    }
+
+    @Collection
+    public List<String> getSampleCollection() {
+        return List.of("Foo", "Bar");
+    }
+
 }
\ No newline at end of file
diff --git 
a/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/LayoutSwitchingTest.java
 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/LayoutSwitchingTest.java
new file mode 100644
index 00000000000..ff69b9dd04f
--- /dev/null
+++ 
b/core/mmtest/src/test/java/org/apache/causeway/core/metamodel/services/grid/LayoutSwitchingTest.java
@@ -0,0 +1,65 @@
+/*
+ *  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;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.apache.causeway.applib.layout.grid.bootstrap.BSUtil;
+import org.apache.causeway.applib.services.grid.GridService;
+import org.apache.causeway.core.metamodel.MetaModelTestAbstract;
+import org.apache.causeway.core.metamodel.facets.object.grid.GridFacet;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+
+/**
+ * Switching between Layout Variants may result in Members staying hidden.
+ * https://issues.apache.org/jira/browse/CAUSEWAY-3971
+ */
+class LayoutSwitchingTest extends MetaModelTestAbstract {
+
+    private GridServiceDefault gridService;
+
+    @Override
+    protected void afterSetUp() {
+        this.gridService = ((GridServiceDefault) getServiceRegistry()
+                .lookupServiceElseFail(GridService.class));
+        assertFalse(gridService.supportsReloading());
+    }
+
+    @Test
+    void switchLayout() {
+
+        var barSpec = getSpecificationLoader().specForTypeElseFail(Bar.class);
+
+        var gridFacet = barSpec.getFacet(GridFacet.class);
+        assertNotNull(gridFacet);
+
+        // triggers grid to be loaded (if initial or reloading supported)
+        var bsGrid = gridFacet.getGrid(ManagedObject.adaptSingular(barSpec, 
new Bar("simple")));
+        assertNotNull(bsGrid);
+
+        var bsGridDefault = 
gridFacet.getGrid(ManagedObject.adaptSingular(barSpec, new Bar()));
+        System.err.println(BSUtil.toYaml(bsGrid)); // we don't expect too see 
any hidden members but there are!
+        //TODO fail on seeing hidden members
+    }
+
+
+}
diff --git 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/layout/bs/BSGridPanelFactory.java
 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/layout/bs/BSGridPanelFactory.java
index 0b61457e266..6d093584e34 100644
--- 
a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/layout/bs/BSGridPanelFactory.java
+++ 
b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/layout/bs/BSGridPanelFactory.java
@@ -54,12 +54,9 @@ public BSGridPanelFactory() {
     @Override
     public Component createComponent(final String id, final IModel<?> model) {
         final UiObjectWkt objectModel = (UiObjectWkt) model;
-
         var mo = objectModel.getObject();
-        var objectSpec = objectModel.getTypeOfSpecification();
 
-        return Facets.bootstrapGrid(objectSpec, mo)
-            .map(UiGridLayout::new) // creates a deep copy of the underlying 
grid and applies rules
+        return UiGridLayout.forObject(mo) // creates a deep copy of the 
underlying grid and applies rules
             .map(ui->new BSGridPanel(id, objectModel, ui.bsGrid()))
             .orElseThrow(); // empty case guarded against by appliesTo(...) 
above
     }

Reply via email to