You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2024/10/29 13:30:28 UTC

(causeway) branch master updated: CAUSEWAY-3815: prepare meta-model to support actions in collection view

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

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


The following commit(s) were added to refs/heads/master by this push:
     new dcbec39cc4d CAUSEWAY-3815: prepare meta-model to support actions in collection view
dcbec39cc4d is described below

commit dcbec39cc4d0d8178b4050834b4628bb36fb2261
Author: Andi Huber <ah...@apache.org>
AuthorDate: Tue Oct 29 14:30:23 2024 +0100

    CAUSEWAY-3815: prepare meta-model to support actions in collection view
---
 .../applib/annotation/CollectionLayout.java        |  5 --
 .../ColumnOrderTxtFileServiceDefault.java          | 26 ++++---
 .../core/metamodel/spec/feature/ObjectAction.java  | 23 ++++++
 .../spec/feature/ObjectActionContainer.java        |  5 ++
 .../metamodel/spec/feature/ObjectAssociation.java  | 17 +----
 .../spec/feature/ObjectAssociationContainer.java   |  2 +-
 .../specimpl/dflt/ObjectSpecificationDefault.java  |  9 ++-
 ...ationsAsColumns.java => _MembersAsColumns.java} | 16 ++++-
 .../core/metamodel/util/WhereContexts.java         | 23 ++++--
 .../ui/components/collection/CollectionPanel.java  |  3 +-
 .../CollectionContentsAsAjaxTablePanel.java        | 46 ++++++++----
 .../ajaxtable/columns/ActionColumn.java            | 82 ++++++++++++++++++++++
 .../columns/AssociationColumnAbstract.java         |  4 +-
 .../ajaxtable/columns/GenericColumn.java           |  2 +
 .../ajaxtable/columns/GenericColumnAbstract.java   | 23 +++++-
 .../ajaxtable/columns/PluralColumn.java            |  4 +-
 .../ajaxtable/columns/SingularColumn.java          |  4 +-
 .../ajaxtable/columns/TitleColumn.java             |  4 +-
 .../ajaxtable/columns/ToggleboxColumn.java         |  4 +-
 19 files changed, 244 insertions(+), 58 deletions(-)

diff --git a/api/applib/src/main/java/org/apache/causeway/applib/annotation/CollectionLayout.java b/api/applib/src/main/java/org/apache/causeway/applib/annotation/CollectionLayout.java
index eee4fbdc3d9..891ec29f0ec 100644
--- a/api/applib/src/main/java/org/apache/causeway/applib/annotation/CollectionLayout.java
+++ b/api/applib/src/main/java/org/apache/causeway/applib/annotation/CollectionLayout.java
@@ -88,11 +88,6 @@ public @interface CollectionLayout {
 
     /**
      * Indicates where in the UI the collection should <i>not</i>not be visible.
-     *
-     * <p>
-     *      Only {@link Where#NOWHERE NOWHERE} or {@link Where#EVERYWHERE EVERYWHERE}/{@link Where#ANYWHERE ANYWHERE}
-     *      apply for collections.
-     * </p>
      */
     Where hidden()
             default Where.NOT_SPECIFIED;
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/columnorder/ColumnOrderTxtFileServiceDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/columnorder/ColumnOrderTxtFileServiceDefault.java
index 87580d29dc2..17d614b6133 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/columnorder/ColumnOrderTxtFileServiceDefault.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/services/columnorder/ColumnOrderTxtFileServiceDefault.java
@@ -27,6 +27,7 @@ import org.springframework.stereotype.Service;
 
 import org.apache.causeway.applib.annotation.PriorityPrecedence;
 import org.apache.causeway.applib.annotation.Programmatic;
+import org.apache.causeway.applib.annotation.Where;
 import org.apache.causeway.applib.services.columnorder.ColumnOrderTxtFileService;
 import org.apache.causeway.commons.io.ZipUtils;
 import org.apache.causeway.core.metamodel.CausewayModuleCoreMetamodel;
@@ -53,6 +54,7 @@ public class ColumnOrderTxtFileServiceDefault implements ColumnOrderTxtFileServi
 
     final SpecificationLoader specificationLoader;
 
+    @Override
     @Programmatic
     public byte[] toZip(final Object domainObject) {
         var zipBuilder = ZipUtils.zipEntryBuilder();
@@ -65,7 +67,7 @@ public class ColumnOrderTxtFileServiceDefault implements ColumnOrderTxtFileServi
 
     // HELPERS
 
-    private void addStandaloneEntry(Object domainObject, final ZipUtils.EntryBuilder zipBuilder) {
+    private void addStandaloneEntry(final Object domainObject, final ZipUtils.EntryBuilder zipBuilder) {
         var parentSpec = specificationLoader.loadSpecification(domainObject.getClass());
         var buf = new StringBuilder();
 
@@ -79,23 +81,25 @@ public class ColumnOrderTxtFileServiceDefault implements ColumnOrderTxtFileServi
         zipBuilder.addAsUtf8(fileName, fileContents);
     }
 
-    private void addCollectionEntries(Object domainObject, final ZipUtils.EntryBuilder zipBuilder) {
+    private void addCollectionEntries(final Object domainObject, final ZipUtils.EntryBuilder zipBuilder) {
         var parentSpec = specificationLoader.loadSpecification(domainObject.getClass());
         parentSpec.streamCollections(MixedIn.INCLUDED)
                 .forEach(collection -> addCollection(collection, parentSpec, zipBuilder));
     }
 
-    private void addCollection(final OneToManyAssociation collection, final ObjectSpecification parentSpec, final ZipUtils.EntryBuilder zipBuilder) {
-        var buf = new StringBuilder();
+    private void addCollection(
+            final OneToManyAssociation collection,
+            final ObjectSpecification parentSpec,
+            final ZipUtils.EntryBuilder zipBuilder) {
 
-        var collectionIdentifier = collection.getFeatureIdentifier();
-        var elementType = collection.getElementType();
+        var buf = new StringBuilder();
 
-        elementType.streamAssociations(MixedIn.INCLUDED)
-                .filter(ObjectAssociation.Predicates.visibleAccordingToHiddenFacet(collectionIdentifier))
-                .filter(ObjectAssociation.Predicates.referencesParent(parentSpec).negate())
-                .map(ObjectFeature::getId)
-                .forEach(assocId -> buf.append(assocId).append("\n"));
+        collection.getElementType()
+            .streamAssociations(MixedIn.INCLUDED)
+            .filter(ObjectAssociation.Predicates.visibleAccordingToHiddenFacet(Where.PARENTED_TABLES))
+            .filter(ObjectAssociation.Predicates.referencesParent(parentSpec).negate())
+            .map(ObjectFeature::getId)
+            .forEach(assocId -> buf.append(assocId).append("\n"));
 
         var fileName = String.format("%s#%s.columnOrder.txt", parentSpec.getShortIdentifier(), collection.getId());
         var fileContents = buf.toString();
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAction.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAction.java
index 46c30f9548f..619b6eba3e5 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAction.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAction.java
@@ -39,8 +39,10 @@ import org.apache.causeway.core.config.CausewayConfiguration.Viewer.Wicket;
 import org.apache.causeway.core.metamodel.consent.Consent;
 import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
 import org.apache.causeway.core.metamodel.consent.InteractionResultSet;
+import org.apache.causeway.core.metamodel.facets.WhereValueFacet;
 import org.apache.causeway.core.metamodel.facets.actions.action.choicesfrom.ChoicesFromFacet;
 import org.apache.causeway.core.metamodel.facets.actions.position.ActionPositionFacet;
+import org.apache.causeway.core.metamodel.facets.all.hide.HiddenFacet;
 import org.apache.causeway.core.metamodel.facets.members.iconfa.FaFacet;
 import org.apache.causeway.core.metamodel.facets.members.iconfa.FaLayersProvider;
 import org.apache.causeway.core.metamodel.facets.members.layout.group.LayoutGroupFacet;
@@ -492,6 +494,27 @@ public interface ObjectAction extends ObjectMember {
                             ));
         }
 
+        /**
+         * Returns true if no {@link HiddenFacet} is found that vetoes visibility.
+         * <p>
+         * However, whereHidden={@link Where#ALL_TABLES} is used as default
+         * when no {@link HiddenFacet} is found.
+         *
+         * @see ObjectAssociation.Predicates#visibleAccordingToHiddenFacet(Where)
+         *
+         * @apiNote an alternative would be to prime the meta-model with fallback facets,
+         *      however the current approach is more heap friendly
+         */
+        public static Predicate<ObjectAction> visibleAccordingToHiddenFacet(final Where whereContext) {
+            return (final ObjectAction act) -> act.lookupFacet(HiddenFacet.class)
+                    .map(WhereValueFacet.class::cast)
+                    .map(WhereValueFacet::where)
+                    // whereHidden=ALL_TABLES is the default when not specified otherwise
+                    .or(()->Optional.of(Where.ALL_TABLES))
+                    .stream()
+                    .noneMatch(whereHidden -> whereHidden.includes(whereContext));
+        }
+
         // -- HELPER
 
         private static class HasChoicesFrom implements Predicate<ObjectAction> {
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectActionContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectActionContainer.java
index 02ed6eb4db9..70107daa2b3 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectActionContainer.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectActionContainer.java
@@ -140,6 +140,11 @@ public interface ObjectActionContainer {
      */
     Stream<ObjectAction> streamRuntimeActions(MixedIn mixedIn);
 
+    /**
+     * Actions visible in dedicated actions-column, honoring order and visibility.
+     */
+    Stream<ObjectAction> streamActionsForColumnRendering(Identifier memberIdentifier);
+
     // -- ACTION STREAM (NO INHERITANCE)
 
     /**
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociation.java
index 7c047520a1f..e19e4557846 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociation.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociation.java
@@ -24,7 +24,6 @@ import java.util.function.Predicate;
 
 import org.springframework.lang.Nullable;
 
-import org.apache.causeway.applib.Identifier;
 import org.apache.causeway.applib.annotation.Domain;
 import org.apache.causeway.applib.annotation.Where;
 import org.apache.causeway.commons.collections.Can;
@@ -42,9 +41,6 @@ import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.util.Facets;
 
-import static org.apache.causeway.applib.annotation.Where.PARENTED_TABLES;
-import static org.apache.causeway.applib.annotation.Where.STANDALONE_TABLES;
-
 /**
  * Provides reflective access to a field on a domain object.
  */
@@ -157,25 +153,18 @@ public interface ObjectAssociation extends ObjectMember, CurrentHolder {
             };
         }
 
-        static Where whereContextFor(final Identifier memberIdentifier) {
-            return memberIdentifier.getType().isAction()
-                    ? STANDALONE_TABLES
-                    : PARENTED_TABLES;
-        }
-
         /**
          * Returns true if no {@link HiddenFacet} is found that vetoes visibility.
-         *
          * <p>
          * However, if it's a 1-to-Many, whereHidden={@link Where#ALL_TABLES} is used as default
          * when no {@link HiddenFacet} is found.
          *
+         * @see ObjectAction.Predicates#visibleAccordingToHiddenFacet(Where)
+         *
          * @apiNote an alternative would be to prime the meta-model with fallback facets,
          *      however the current approach is more heap friendly
          */
-        public static Predicate<ObjectAssociation> visibleAccordingToHiddenFacet(
-                final Identifier memberIdentifier) {
-            var whereContext = whereContextFor(memberIdentifier);
+        public static Predicate<ObjectAssociation> visibleAccordingToHiddenFacet(final Where whereContext) {
             return (final ObjectAssociation assoc) -> assoc.lookupFacet(HiddenFacet.class)
                     .map(WhereValueFacet.class::cast)
                     .map(WhereValueFacet::where)
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java
index 6096e63d9b2..73928317e06 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/spec/feature/ObjectAssociationContainer.java
@@ -131,7 +131,7 @@ public interface ObjectAssociationContainer {
     }
 
     /**
-     * Properties and Collections visible as columns honoring order and visibility.
+     * Properties and Collections visible as columns, honoring order and visibility.
      */
     Stream<ObjectAssociation> streamAssociationsForColumnRendering(
             Identifier memberIdentifier,
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
index 98cd420d80b..d6538833722 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/ObjectSpecificationDefault.java
@@ -316,10 +316,17 @@ implements FacetHolder {
             final Identifier memberIdentifier,
             final ManagedObject parentObject) {
 
-        return new _AssociationsAsColumns(getMetaModelContext())
+        return new _MembersAsColumns(getMetaModelContext())
             .streamAssociationsForColumnRendering(this, memberIdentifier, parentObject);
     }
 
+    @Override
+    public Stream<ObjectAction> streamActionsForColumnRendering(
+            final Identifier memberIdentifier) {
+        return new _MembersAsColumns(getMetaModelContext())
+                .streamActionsForColumnRendering(this, memberIdentifier);
+    }
+
     // -- DETERMINE INJECTABILITY
 
     private boolean isVetoedForInjection;
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/_AssociationsAsColumns.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/_MembersAsColumns.java
similarity index 90%
rename from core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/_AssociationsAsColumns.java
rename to core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/_MembersAsColumns.java
index 64cd6e05e5c..d174d16193d 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/_AssociationsAsColumns.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/dflt/_MembersAsColumns.java
@@ -38,7 +38,9 @@ import org.apache.causeway.core.metamodel.facets.object.grid.GridFacet;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 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;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
+import org.apache.causeway.core.metamodel.util.WhereContexts;
 
 import static org.apache.causeway.applib.annotation.Where.PARENTED_TABLES;
 import static org.apache.causeway.applib.annotation.Where.STANDALONE_TABLES;
@@ -48,11 +50,21 @@ import lombok.NonNull;
 import lombok.RequiredArgsConstructor;
 
 @RequiredArgsConstructor
-class _AssociationsAsColumns implements HasMetaModelContext {
+class _MembersAsColumns implements HasMetaModelContext {
 
     @Getter(onMethod_ = {@Override})
     private final MetaModelContext metaModelContext;
 
+    public final Stream<ObjectAction> streamActionsForColumnRendering(
+            final ObjectSpecification elementType,
+            final Identifier memberIdentifier) {
+        if(elementType.isValue()) return Stream.empty();
+
+        return elementType.streamRuntimeActions(MixedIn.INCLUDED)
+                .filter(ObjectAction.Predicates.visibleAccordingToHiddenFacet(WhereContexts.collectionVariant(memberIdentifier)))
+                .sorted((a, b)->a.getCanonicalFriendlyName().compareTo(b.getCanonicalFriendlyName()));
+    }
+
     public final Stream<ObjectAssociation> streamAssociationsForColumnRendering(
             final ObjectSpecification elementType,
             final Identifier memberIdentifier,
@@ -66,7 +78,7 @@ class _AssociationsAsColumns implements HasMetaModelContext {
         var assocById = _Maps.<String, ObjectAssociation>newLinkedHashMap();
 
         elementType.streamAssociations(MixedIn.INCLUDED)
-        .filter(ObjectAssociation.Predicates.visibleAccordingToHiddenFacet(memberIdentifier))
+        .filter(ObjectAssociation.Predicates.visibleAccordingToHiddenFacet(WhereContexts.collectionVariant(memberIdentifier)))
         .filter(ObjectAssociation.Predicates.referencesParent(parentSpecIfAny).negate())
         .filter(assoc->filterColumnsUsingSpi(assoc, elementClass)) // optional SPI to filter columns;
         .forEach(assoc->assocById.put(assoc.getId(), assoc));
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/GenericColumn.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/WhereContexts.java
similarity index 53%
copy from viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/GenericColumn.java
copy to core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/WhereContexts.java
index 3a9bc03a4c0..2e521e21bf8 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/GenericColumn.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/util/WhereContexts.java
@@ -16,13 +16,26 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.viewer.wicket.ui.components.collectioncontents.ajaxtable.columns;
+package org.apache.causeway.core.metamodel.util;
 
-import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
+import org.apache.causeway.applib.Identifier;
+import org.apache.causeway.applib.annotation.Where;
 
-import org.apache.causeway.core.metamodel.tabular.DataRow;
+import lombok.experimental.UtilityClass;
 
-public interface GenericColumn
-extends IColumn<DataRow, String> {
+@UtilityClass
+public class WhereContexts {
+
+    /**
+     * Utility, that detects the collection variant (standalone or parented),
+     * based on the feature {@link Identifier} of the originating feature,
+     * that is, the feature is either a plural member or a plural action result.
+     */
+    public Where collectionVariant(final Identifier featureId) {
+        var whereContext = featureId.getType().isAction()
+                    ? Where.STANDALONE_TABLES
+                    : Where.PARENTED_TABLES;
+        return whereContext;
+    }
 
 }
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collection/CollectionPanel.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collection/CollectionPanel.java
index bd61bd31b49..9aa2e605649 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collection/CollectionPanel.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collection/CollectionPanel.java
@@ -36,6 +36,7 @@ import org.apache.causeway.viewer.wicket.ui.panels.PanelAbstract;
 
 import lombok.Getter;
 import lombok.Setter;
+
 import de.agilecoders.wicket.core.markup.html.bootstrap.common.NotificationPanel;
 
 /**
@@ -94,7 +95,7 @@ implements
             var collModel = getModel();
             var collMetaModel = collModel.getMetaModel();
             toggleboxColumn =  collMetaModel.hasAssociatedActionsWithChoicesFromThisCollection()
-                    ? Optional.of(new ToggleboxColumn(collModel.delegate()))
+                    ? Optional.of(new ToggleboxColumn(collModel.getElementType(), collModel.delegate()))
                     : Optional.empty();
         }
         return toggleboxColumn.orElse(null);
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CollectionContentsAsAjaxTablePanel.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CollectionContentsAsAjaxTablePanel.java
index d2fff1a3d44..30797c8fe91 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CollectionContentsAsAjaxTablePanel.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/CollectionContentsAsAjaxTablePanel.java
@@ -31,6 +31,7 @@ import org.apache.wicket.request.resource.CssResourceReference;
 import org.apache.causeway.commons.internal.collections._Lists;
 import org.apache.causeway.core.config.CausewayConfiguration.Viewer.Wicket;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
 import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
@@ -39,6 +40,7 @@ import org.apache.causeway.viewer.wicket.model.models.EntityCollectionModel;
 import org.apache.causeway.viewer.wicket.model.models.EntityCollectionModel.Variant;
 import org.apache.causeway.viewer.wicket.ui.components.collection.bulk.MultiselectToggleProvider;
 import org.apache.causeway.viewer.wicket.ui.components.collection.count.CollectionCountProvider;
+import org.apache.causeway.viewer.wicket.ui.components.collectioncontents.ajaxtable.columns.ActionColumn;
 import org.apache.causeway.viewer.wicket.ui.components.collectioncontents.ajaxtable.columns.ColumnAbbreviationOptions;
 import org.apache.causeway.viewer.wicket.ui.components.collectioncontents.ajaxtable.columns.GenericColumn;
 import org.apache.causeway.viewer.wicket.ui.components.collectioncontents.ajaxtable.columns.PluralColumn;
@@ -107,20 +109,26 @@ implements CollectionCountProvider {
         final List<GenericColumn> columns = _Lists.newArrayList();
 
         var collectionModel = entityCollectionModel();
+        var elementType = collectionModel.getElementType();
 
         // first create property columns, so we know how many columns there are
         addPropertyColumnsIfRequired(columns);
         // prepend title column, which may have distinct rendering hints,
         // based on whether there are any property columns or not
         prependTitleColumn(
-                columns,
+                elementType,
                 collectionModel.getVariant(),
-                getWicketViewerSettings());
+                getWicketViewerSettings(),
+                columns);
 
-        // prepend togglebox column (left most), if enabled
+        // prepend toggle-box column (left most), if enabled
         getToggleboxColumn()
             .ifPresent(toggleboxColumn->columns.add(0, toggleboxColumn));
 
+        // last append action column
+        //TODO[CAUSEWAY-3815] disabled until action column rendering is fleshed out
+        //addActionsColumnIfRequired(elementType, columns);
+
         var dataProvider = new CollectionContentsSortableDataProvider(collectionModel);
         var dataTable = new CausewayAjaxDataTable(
                 ID_TABLE, columns, dataProvider, collectionModel.getPageSize());
@@ -157,9 +165,10 @@ implements CollectionCountProvider {
     }
 
     private void prependTitleColumn(
-            final List<GenericColumn> columns,
+            final ObjectSpecification elementType,
             final Variant variant,
-            final Wicket wktConfig) {
+            final Wicket wktConfig,
+            final List<GenericColumn> columns) {
 
         var contextBookmark = entityCollectionModel().getParentObject().getBookmark()
                 .orElse(null);
@@ -174,14 +183,15 @@ implements CollectionCountProvider {
                             : -1 /* don't override */)
             .build();
 
-        columns.add(0, new TitleColumn(variant, contextBookmark, maxColumnTitleLength, opts));
+        columns.add(0, new TitleColumn(elementType, variant, contextBookmark, maxColumnTitleLength, opts));
     }
 
-    private void addPropertyColumnsIfRequired(final List<GenericColumn> columns) {
+    private void addPropertyColumnsIfRequired(
+            final List<GenericColumn> columns) {
 
         var collectionModel = getModel();
-        var elementTypeSpec = collectionModel.getElementType();
-        if(elementTypeSpec == null) {
+        var elementType = collectionModel.getElementType();
+        if(elementType == null) {
             return;
         }
 
@@ -189,7 +199,7 @@ implements CollectionCountProvider {
         var memberIdentifier = collectionModel.getIdentifier();
 
         // add all ordered columns to the table
-        elementTypeSpec.streamAssociationsForColumnRendering(memberIdentifier, parentObject)
+        elementType.streamAssociationsForColumnRendering(memberIdentifier, parentObject)
         .map(ObjectAssociation::getSpecialization)
         .map(spez->spez.fold(
                 this::createSingularColumn,
@@ -197,7 +207,8 @@ implements CollectionCountProvider {
         .forEach(columns::add);
     }
 
-    private SingularColumn createSingularColumn(final OneToOneAssociation property) {
+    private SingularColumn createSingularColumn(
+            final OneToOneAssociation property) {
         var collectionModel = getModel();
         final String parentTypeName = property.getDeclaringType().getLogicalTypeName();
         final Optional<String> sortability = property.getElementType().isComparableOrOrdered()
@@ -205,6 +216,7 @@ implements CollectionCountProvider {
                 : Optional.empty();
 
         return new SingularColumn(
+                collectionModel.getElementType(),
                 collectionModel.getVariant(),
                 Model.of(property.getCanonicalFriendlyName()),
                 sortability,
@@ -213,11 +225,13 @@ implements CollectionCountProvider {
                 property.getCanonicalDescription());
     }
 
-    private PluralColumn createPluralColumn(final OneToManyAssociation collection) {
+    private PluralColumn createPluralColumn(
+            final OneToManyAssociation collection) {
         var collectionModel = getModel();
         final String parentTypeName = collection.getDeclaringType().getLogicalTypeName();
 
         return new PluralColumn(
+                collectionModel.getElementType(),
                 collectionModel.getVariant(),
                 Model.of(collection.getCanonicalFriendlyName()),
                 collection.getId(),
@@ -227,4 +241,12 @@ implements CollectionCountProvider {
                 RenderOptions.builder().build());
     }
 
+    private void addActionsColumnIfRequired(
+            final ObjectSpecification elementType,
+            final List<GenericColumn> columns) {
+        var collectionModel = getModel();
+        var memberIdentifier = collectionModel.getIdentifier();
+        ActionColumn.create(memberIdentifier, elementType).ifPresent(columns::add);
+    }
+
 }
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/ActionColumn.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/ActionColumn.java
new file mode 100644
index 00000000000..2d7c6bc5499
--- /dev/null
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/ActionColumn.java
@@ -0,0 +1,82 @@
+/*
+ *  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.viewer.wicket.ui.components.collectioncontents.ajaxtable.columns;
+
+import java.util.Optional;
+
+import org.apache.wicket.Component;
+
+import org.apache.causeway.applib.Identifier;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.viewer.wicket.model.models.interaction.coll.DataRowWkt;
+import org.apache.causeway.viewer.wicket.ui.util.Wkt;
+
+import lombok.NonNull;
+
+public final class ActionColumn
+extends GenericColumnAbstract {
+
+    private static final long serialVersionUID = 1L;
+
+    public static Optional<ActionColumn> create(
+            @NonNull final Identifier featureId,
+            @NonNull final ObjectSpecification elementType) {
+        var actions = elementType.streamActionsForColumnRendering(featureId)
+                .collect(Can.toCan());
+        if(actions.isEmpty()) return Optional.empty();
+        return Optional.of(new ActionColumn(elementType, actions));
+    }
+
+    private final Can<String> actionIds;
+    private transient Can<ObjectAction> actions;
+
+    private ActionColumn(
+            final ObjectSpecification elementType,
+            final Can<ObjectAction> actionsForColumnRendering) {
+        super(elementType, "Actions");
+        this.actions = actionsForColumnRendering;
+        this.actionIds = actions.map(ObjectAction::getId);
+    }
+
+    @Override
+    protected Component createCellComponent(
+            final String componentId, final DataRowWkt dataRowWkt) {
+        var dataRow = dataRowWkt.getObject();
+        var rowElement = dataRow.getRowElement();
+
+        return Wkt.label(componentId, actions()
+                .map(action->action.getFriendlyName(()->rowElement))
+                .join("|"));
+    }
+
+    // -- HELPER
+
+    private Can<ObjectAction> actions() {
+        synchronized(this) {
+            if(actions==null) {
+                var elementTypeSpec = elementType();
+                this.actions = actionIds.map(elementTypeSpec::getActionElseFail);
+            }
+        }
+        return actions;
+    }
+
+}
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/AssociationColumnAbstract.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/AssociationColumnAbstract.java
index 6a4f1310b2d..ef2248442de 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/AssociationColumnAbstract.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/AssociationColumnAbstract.java
@@ -26,6 +26,7 @@ import org.apache.wicket.model.IModel;
 
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.viewer.wicket.model.models.EntityCollectionModel;
 import org.apache.causeway.viewer.wicket.ui.ComponentFactory;
 import org.apache.causeway.viewer.wicket.ui.app.registry.ComponentFactoryRegistry;
@@ -53,6 +54,7 @@ extends GenericColumnAbstract {
     protected final String describedAs;
 
     public AssociationColumnAbstract(
+            final ObjectSpecification elementType,
             final EntityCollectionModel.Variant collectionVariant,
             final IModel<String> columnNameModel,
             /**
@@ -66,7 +68,7 @@ extends GenericColumnAbstract {
             final String parentTypeName,
             final Optional<String> describedAs) {
 
-        super(columnNameModel, sortProperty.orElse(null));
+        super(elementType, columnNameModel, sortProperty.orElse(null));
         this.collectionVariant = collectionVariant;
         this.memberId = memberId;
         this.parentTypeName = parentTypeName;
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/GenericColumn.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/GenericColumn.java
index 3a9bc03a4c0..d9f648aefbc 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/GenericColumn.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/GenericColumn.java
@@ -20,9 +20,11 @@ package org.apache.causeway.viewer.wicket.ui.components.collectioncontents.ajaxt
 
 import org.apache.wicket.extensions.markup.html.repeater.data.table.IColumn;
 
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.tabular.DataRow;
 
 public interface GenericColumn
 extends IColumn<DataRow, String> {
 
+    ObjectSpecification elementType();
 }
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/GenericColumnAbstract.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/GenericColumnAbstract.java
index 439cdfafd2d..ee2ef0c92e4 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/GenericColumnAbstract.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/GenericColumnAbstract.java
@@ -30,6 +30,8 @@ import org.apache.wicket.model.Model;
 
 import org.apache.causeway.applib.services.i18n.TranslationContext;
 import org.apache.causeway.core.metamodel.context.HasMetaModelContext;
+import org.apache.causeway.core.metamodel.context.MetaModelContext;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.tabular.DataRow;
 import org.apache.causeway.viewer.commons.model.components.UiComponentType;
 import org.apache.causeway.viewer.wicket.model.models.interaction.coll.DataRowWkt;
@@ -52,16 +54,22 @@ implements GenericColumn, HasMetaModelContext {
     private static final long serialVersionUID = 1L;
 
     private transient ComponentFactoryRegistry componentRegistry;
+    private final Class<?> elementClass;
+    private transient ObjectSpecification elementType;
 
     protected GenericColumnAbstract(
+            final ObjectSpecification elementType,
             final String columnName) {
-        this(Model.of(columnName), null);
+        this(elementType, Model.of(columnName), null);
     }
 
     protected GenericColumnAbstract(
+            final ObjectSpecification elementType,
             final IModel<String> columnNameModel,
             final String sortColumn) {
         super(columnNameModel, sortColumn);
+        this.elementType = elementType;
+        this.elementClass = elementType.getCorrespondingClass();
     }
 
     @Override
@@ -70,6 +78,9 @@ implements GenericColumn, HasMetaModelContext {
             final String componentId,
             final IModel<DataRow> rowModel) {
         cellItem.add(createCellComponent(componentId, (DataRowWkt)rowModel));
+        if(this instanceof ActionColumn) {
+            Wkt.cssAppend(cellItem, "action-column");
+        }
         if(this instanceof TitleColumn) {
             Wkt.cssAppend(cellItem, "title-column");
             if(((TitleColumn)this).isTitleSuppressed()) {
@@ -85,6 +96,16 @@ implements GenericColumn, HasMetaModelContext {
 
     protected abstract Component createCellComponent(final String componentId, final DataRowWkt dataRowWkt);
 
+    @Override
+    public final ObjectSpecification elementType() {
+        synchronized(this) {
+            if(elementType==null) {
+                this.elementType = MetaModelContext.instanceElseFail().specForTypeElseFail(elementClass);
+            }
+        }
+        return elementType;
+    }
+
     protected ComponentFactory findComponentFactory(final UiComponentType uiComponentType, final IModel<?> model) {
         return getComponentRegistry().findComponentFactory(uiComponentType, model);
     }
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/PluralColumn.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/PluralColumn.java
index 4dd45788896..a77863f4fdf 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/PluralColumn.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/PluralColumn.java
@@ -31,6 +31,7 @@ import org.apache.causeway.applib.services.linking.DeepLinkService;
 import org.apache.causeway.applib.services.placeholder.PlaceholderRenderService.PlaceholderLiteral;
 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.tabular.DataColumn;
 import org.apache.causeway.viewer.commons.model.components.UiComponentType;
 import org.apache.causeway.viewer.wicket.model.models.EntityCollectionModel;
@@ -61,13 +62,14 @@ extends AssociationColumnAbstract {
     private final RenderOptions opts;
 
     public PluralColumn(
+            final ObjectSpecification elementType,
             final EntityCollectionModel.Variant collectionVariant,
             final IModel<String> columnNameModel,
             final String propertyId,
             final String parentTypeName,
             final Optional<String> describedAs,
             final RenderOptions opts) {
-        super(collectionVariant, columnNameModel,
+        super(elementType, collectionVariant, columnNameModel,
                 Optional.empty(), // empty sortProperty (hence never sortable)
                 propertyId, parentTypeName, describedAs);
         this.opts = opts;
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/SingularColumn.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/SingularColumn.java
index a77e0863eba..ecd25dbce94 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/SingularColumn.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/SingularColumn.java
@@ -24,6 +24,7 @@ import org.apache.wicket.Component;
 import org.apache.wicket.model.IModel;
 
 import org.apache.causeway.core.metamodel.commons.ViewOrEditMode;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.viewer.commons.model.components.UiComponentType;
 import org.apache.causeway.viewer.wicket.model.models.EntityCollectionModel;
 import org.apache.causeway.viewer.wicket.model.models.UiObjectWkt;
@@ -35,13 +36,14 @@ extends AssociationColumnAbstract {
     private static final long serialVersionUID = 1L;
 
     public SingularColumn(
+            final ObjectSpecification elementType,
             final EntityCollectionModel.Variant collectionVariant,
             final IModel<String> columnNameModel,
             final Optional<String> sortProperty,
             final String propertyId,
             final String parentTypeName,
             final Optional<String> describedAs) {
-        super(collectionVariant, columnNameModel, sortProperty, propertyId, parentTypeName, describedAs);
+        super(elementType, collectionVariant, columnNameModel, sortProperty, propertyId, parentTypeName, describedAs);
     }
 
     @Override
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/TitleColumn.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/TitleColumn.java
index 8efaaa18854..dccd9a0c0ac 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/TitleColumn.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/TitleColumn.java
@@ -24,6 +24,7 @@ import org.springframework.lang.Nullable;
 
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.core.metamodel.object.ManagedObjects;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.viewer.commons.model.components.UiComponentType;
 import org.apache.causeway.viewer.wicket.model.models.EntityCollectionModel.Variant;
 import org.apache.causeway.viewer.wicket.model.models.UiObjectWkt;
@@ -39,12 +40,13 @@ extends GenericColumnAbstract {
     private final Bookmark contextBookmark;
 
     public TitleColumn(
+            final ObjectSpecification elementType,
             final Variant variant,
             final Bookmark contextBookmark,
             final int maxColumnTitleLength,
             final ColumnAbbreviationOptions opts) {
 
-        super(columnName(variant, maxColumnTitleLength)); // i18n
+        super(elementType, columnName(variant, maxColumnTitleLength)); // i18n
         this.variant = variant;
         this.contextBookmark = contextBookmark;
         this.opts = opts;
diff --git a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/ToggleboxColumn.java b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/ToggleboxColumn.java
index bad078d63cb..96af6228994 100644
--- a/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/ToggleboxColumn.java
+++ b/viewers/wicket/ui/src/main/java/org/apache/causeway/viewer/wicket/ui/components/collectioncontents/ajaxtable/columns/ToggleboxColumn.java
@@ -25,6 +25,7 @@ import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.model.IModel;
 
 import org.apache.causeway.commons.internal.collections._Lists;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.tabular.DataTableInteractive;
 import org.apache.causeway.viewer.wicket.model.models.interaction.coll.DataRowToggleWkt;
 import org.apache.causeway.viewer.wicket.model.models.interaction.coll.DataRowWkt;
@@ -48,8 +49,9 @@ extends GenericColumnAbstract {
     private final List<ContainedToggleboxPanel> rowToggles = _Lists.newArrayList();
 
     public ToggleboxColumn(
+            final ObjectSpecification elementType,
             final IModel<DataTableInteractive> dataTableModelHolder) {
-        super("");
+        super(elementType, "");
         this.dataTableModelHolder = dataTableModelHolder;
     }