You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by da...@apache.org on 2026/06/17 09:33:28 UTC
(causeway) branch CAUSEWAY-4038 updated (9937366e891 -> d80cda55d2f)
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a change to branch CAUSEWAY-4038
in repository https://gitbox.apache.org/repos/asf/causeway.git
from 9937366e891 CAUSEWAY-4038 : reworks ordering of params once more
new 7a8d749f2b2 CAUSEWAY-4038: defers navigate-to ordering and pads replay args
new d90085d5c47 CAUSEWAY-4038: archives navigate-to parameter OpenSpec change
new d80cda55d2f CAUSEWAY-4038: surfaces 'target' for ReplayableCommand
The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
...OrderFacetForParentedCollectionNavigation.java} | 33 ++++-
.../specimpl/ObjectSpecificationAbstract.java | 2 +
...ParentedCollectionNavigationActionUtilTest.java | 23 ++++
.../command/CommandExecutorServiceDefault.java | 40 +++++-
.../command/CommandExecutorServiceDefaultTest.java | 144 +++++++++++++++++++++
.../ReplayableCommand.columnOrder.fallback.txt | 1 +
.../applib/dom/replay/ReplayableCommand.java | 18 +++
.../.openspec.yaml | 0
.../design.md | 0
.../proposal.md | 0
.../parented-collection-selector-actions/spec.md | 0
.../tasks.md | 0
.../parented-collection-selector-actions/spec.md | 44 +++++++
13 files changed, 297 insertions(+), 8 deletions(-)
copy core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/synthetic/{ParentedCollectionNavigationFacetDefault.java => LayoutOrderFacetForParentedCollectionNavigation.java} (61%)
create mode 100644 core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefaultTest.java
rename openspec/changes/{improve-generated-navigate-to-parameters => archive/2026-06-17-improve-generated-navigate-to-parameters}/.openspec.yaml (100%)
rename openspec/changes/{improve-generated-navigate-to-parameters => archive/2026-06-17-improve-generated-navigate-to-parameters}/design.md (100%)
rename openspec/changes/{improve-generated-navigate-to-parameters => archive/2026-06-17-improve-generated-navigate-to-parameters}/proposal.md (100%)
rename openspec/changes/{improve-generated-navigate-to-parameters => archive/2026-06-17-improve-generated-navigate-to-parameters}/specs/parented-collection-selector-actions/spec.md (100%)
rename openspec/changes/{improve-generated-navigate-to-parameters => archive/2026-06-17-improve-generated-navigate-to-parameters}/tasks.md (100%)
(causeway) 03/03: CAUSEWAY-4038: surfaces 'target' for ReplayableCommand
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-4038
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit d80cda55d2faa731077d4ce8b98863bb2e280911
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Wed Jun 17 10:32:03 2026 +0100
CAUSEWAY-4038: surfaces 'target' for ReplayableCommand
---
.../replay/ReplayableCommand.columnOrder.fallback.txt | 1 +
.../applib/dom/replay/ReplayableCommand.java | 18 ++++++++++++++++++
2 files changed, 19 insertions(+)
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt
index 01c5e912ed4..e78a9abe4d7 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.columnOrder.fallback.txt
@@ -1,5 +1,6 @@
#interactionId
timestamp
+target
member
replayState
hasResult
diff --git a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
index 6e19e781ff0..a8792965efa 100644
--- a/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
+++ b/extensions/core/commandlog/applib/src/main/java/org/apache/causeway/extensions/commandlog/applib/dom/replay/ReplayableCommand.java
@@ -65,6 +65,7 @@
import org.apache.causeway.schema.cmd.v2.MemberDto;
import org.apache.causeway.schema.cmd.v2.ParamDto;
import org.apache.causeway.schema.common.v2.OidDto;
+import org.apache.causeway.schema.common.v2.OidsDto;
import org.apache.causeway.schema.common.v2.ValueType;
import org.apache.causeway.extensions.commandlog.applib.dom.replay.ReplayableCommandParticipant.Role;
import org.apache.causeway.valuetypes.asciidoc.applib.value.AsciiDoc;
@@ -276,6 +277,23 @@ public Optional<ZonedDateTime> getTimestampIfAny() {
.map(JavaTimeXMLGregorianCalendarMarshalling::toZonedDateTime);
}
+ @Property
+ @PropertyLayout(
+ sequence = "3.0",
+ fieldSetId = "details",
+ hidden = Where.OBJECT_FORMS,
+ describedAs = "Target of the command")
+ public String getTarget() {
+ return commandRecord()
+ .map(CommandRecord::commandDto)
+ .map(CommandDto::getTargets)
+ .map(OidsDto::getOid) // returns a list of OidDto's, in fact
+ .flatMap(oidDtoList -> Optional.ofNullable(oidDtoList.isEmpty() ? null : oidDtoList.get(0)))
+ .map(Bookmark::forOidDto)
+ .map(Bookmark::stringify)
+ .orElse(null);
+ }
+
@Property
@PropertyLayout(
sequence = "3.1",
(causeway) 01/03: CAUSEWAY-4038: defers navigate-to ordering and pads replay args
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-4038
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 7a8d749f2b2849553f8a3299f98603ebc6a854ad
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Tue Jun 16 23:23:17 2026 +0100
CAUSEWAY-4038: defers navigate-to ordering and pads replay args
---
...tOrderFacetForParentedCollectionNavigation.java | 66 ++++++++++
.../specimpl/ObjectSpecificationAbstract.java | 2 +
...ParentedCollectionNavigationActionUtilTest.java | 23 ++++
.../command/CommandExecutorServiceDefault.java | 40 +++++-
.../command/CommandExecutorServiceDefaultTest.java | 144 +++++++++++++++++++++
5 files changed, 274 insertions(+), 1 deletion(-)
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/synthetic/LayoutOrderFacetForParentedCollectionNavigation.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/synthetic/LayoutOrderFacetForParentedCollectionNavigation.java
new file mode 100644
index 00000000000..b45e9f74637
--- /dev/null
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/facets/actions/synthetic/LayoutOrderFacetForParentedCollectionNavigation.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.core.metamodel.facets.actions.synthetic;
+
+import java.util.function.BiConsumer;
+
+import org.apache.causeway.core.metamodel.facetapi.Facet;
+import org.apache.causeway.core.metamodel.facetapi.FacetAbstract;
+import org.apache.causeway.core.metamodel.facetapi.FacetHolder;
+import org.apache.causeway.core.metamodel.facets.members.layout.order.LayoutOrderFacet;
+import org.apache.causeway.core.metamodel.spec.feature.OneToManyAssociation;
+
+import lombok.NonNull;
+
+/**
+ * Defers the synthetic navigation action's layout order to the collection it is associated with.
+ */
+public class LayoutOrderFacetForParentedCollectionNavigation
+extends FacetAbstract
+implements LayoutOrderFacet {
+
+ private static final String FALLBACK_SEQUENCE = "1";
+
+ private static Class<? extends Facet> type() {
+ return LayoutOrderFacet.class;
+ }
+
+ private final @NonNull OneToManyAssociation collection;
+
+ public LayoutOrderFacetForParentedCollectionNavigation(
+ final @NonNull OneToManyAssociation collection,
+ final @NonNull FacetHolder holder) {
+ super(type(), holder, Facet.Precedence.INFERRED);
+ this.collection = collection;
+ }
+
+ @Override
+ public String getSequence() {
+ return collection.lookupFacet(LayoutOrderFacet.class)
+ .map(LayoutOrderFacet::getSequence)
+ .orElse(FALLBACK_SEQUENCE);
+ }
+
+ @Override
+ public void visitAttributes(final BiConsumer<String, Object> visitor) {
+ super.visitAttributes(visitor);
+ visitor.accept("sequence", getSequence());
+ }
+
+}
diff --git a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
index 0eb649d9336..c1354ab6d3e 100644
--- a/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
+++ b/core/metamodel/src/main/java/org/apache/causeway/core/metamodel/specloader/specimpl/ObjectSpecificationAbstract.java
@@ -39,6 +39,7 @@
import org.apache.causeway.core.metamodel.facets.actions.synthetic.FaFacetForParentedCollectionNavigation;
import org.apache.causeway.core.metamodel.facets.actions.synthetic.LayoutGroupFacetForParentedCollectionNavigation;
import org.apache.causeway.core.metamodel.facets.actions.synthetic.LayoutGroupFacetForScalarReferenceNavigation;
+import org.apache.causeway.core.metamodel.facets.actions.synthetic.LayoutOrderFacetForParentedCollectionNavigation;
import org.apache.causeway.core.metamodel.facets.actions.synthetic.ParamNamedFacetForParentedCollectionNavigation;
import org.apache.causeway.core.metamodel.facets.actions.synthetic.ParentedCollectionNavigationFacetDefault;
import org.apache.causeway.core.metamodel.facets.actions.synthetic.ScalarReferenceNavigationFacetDefault;
@@ -1193,6 +1194,7 @@ private static void installActionFacets(
FacetUtil.addFacet(new FaFacetForParentedCollectionNavigation(facetedMethod));
FacetUtil.addFacet(new LayoutGroupFacetForParentedCollectionNavigation(
collection.getId(), collection.getCanonicalFriendlyName(), facetedMethod));
+ FacetUtil.addFacet(new LayoutOrderFacetForParentedCollectionNavigation(collection, facetedMethod));
FacetUtil.addFacet(new ParentedCollectionNavigationFacetDefault(collection, facetedMethod));
FacetUtil.addFacet(new DisabledFacetForEmptyParentedCollectionNavigation(collection, facetedMethod));
FacetUtil.addFacet(new ActionSemanticsFacetForParentedCollectionNavigation(facetedMethod));
diff --git a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/specloader/specimpl/ParentedCollectionNavigationActionUtilTest.java b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/specloader/specimpl/ParentedCollectionNavigationActionUtilTest.java
index dbeb01939fa..c247302aaea 100644
--- a/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/specloader/specimpl/ParentedCollectionNavigationActionUtilTest.java
+++ b/core/metamodel/src/test/java/org/apache/causeway/core/metamodel/specloader/specimpl/ParentedCollectionNavigationActionUtilTest.java
@@ -44,6 +44,7 @@
import org.apache.causeway.applib.util.schema.CommandDtoUtils;
import org.apache.causeway.applib.annotation.Bounding;
+import org.apache.causeway.applib.annotation.CollectionLayout;
import org.apache.causeway.applib.annotation.DomainObject;
import org.apache.causeway.applib.annotation.MemberSupport;
import org.apache.causeway.applib.annotation.Nature;
@@ -60,6 +61,8 @@
import org.apache.causeway.core.metamodel._testing.MetaModelContext_forTesting;
import org.apache.causeway.core.metamodel.consent.Consent;
import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy;
+import org.apache.causeway.core.metamodel.facetapi.Facet;
+import org.apache.causeway.core.metamodel.facetapi.FacetUtil;
import org.apache.causeway.core.metamodel.execution.MemberExecutorService;
import org.apache.causeway.core.metamodel.facets.actions.action.invocation.ActionInvocationFacet;
import org.apache.causeway.core.metamodel.facets.actions.action.invocation.IdentifierUtil;
@@ -68,6 +71,8 @@
import org.apache.causeway.core.metamodel.facets.members.disabled.DisabledFacet;
import org.apache.causeway.core.metamodel.facets.members.iconfa.FaFacet;
import org.apache.causeway.core.metamodel.facets.members.layout.group.LayoutGroupFacet;
+import org.apache.causeway.core.metamodel.facets.members.layout.order.LayoutOrderFacet;
+import org.apache.causeway.core.metamodel.facets.members.layout.order.LayoutOrderFacetForLayoutXml;
import org.apache.causeway.core.metamodel.facets.members.publish.command.CommandPublishingFacet;
import org.apache.causeway.core.metamodel.object.ManagedObject;
import org.apache.causeway.core.metamodel.services.publishing.CommandPublisher;
@@ -210,6 +215,7 @@ static class EntityLeaseItem {
@DomainObject(nature = Nature.VIEW_MODEL)
static class OrderedLease {
@Getter
+ @CollectionLayout(sequence = "7")
private final List<OrderedLeaseItem> items = new ArrayList<>();
}
@@ -396,6 +402,23 @@ void orders_scalar_child_parameters_using_static_collection_column_order() {
assertThat(parameters.getElseFail(2).getId(), is("sequence"));
}
+ @Test
+ void derives_navigation_action_member_order_from_associated_collection_lazily() {
+ val orderedMmc = newMetamodelContext();
+ orderedMmc.getConfiguration().getExtensions().getCommandLog().setRecordingSupport(RecordingSupport.ENABLED);
+ val orderedLeaseSpec = orderedMmc.getSpecificationLoader().loadSpecification(OrderedLease.class);
+ val orderedNavigationAction = orderedLeaseSpec.getAction(
+ ObjectSpecificationAbstract.ParentedCollectionNavigationActionUtil.ACTION_ID_PREFIX + "items").orElseThrow();
+ val layoutOrderFacet = orderedNavigationAction.getFacet(LayoutOrderFacet.class);
+ val collection = orderedLeaseSpec.getCollectionElseFail("items");
+
+ assertThat(layoutOrderFacet.getSequence(), is("7"));
+
+ FacetUtil.addFacet(LayoutOrderFacetForLayoutXml.create(42, collection, Facet.Precedence.HIGH, null));
+
+ assertThat(layoutOrderFacet.getSequence(), is("42"));
+ }
+
@Test
void exposes_mandatory_boolean_child_parameter_as_optional_boxed_filter() {
val checkboxParameter = navigationAction.getParameters().stream()
diff --git a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java
index e518fe2989a..38a639228d1 100644
--- a/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java
+++ b/core/runtimeservices/src/main/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefault.java
@@ -18,6 +18,8 @@
*/
package org.apache.causeway.core.runtimeservices.command;
+import java.util.ArrayList;
+import java.util.HashMap;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -89,6 +91,8 @@ public class CommandExecutorServiceDefault implements CommandExecutorService {
private static final Pattern ID_PARSER =
Pattern.compile("(?<className>[^#]+)#?(?<localId>[^(]+)(?<args>[(][^)]*[)])?");
+ private static final String PARENTED_COLLECTION_NAVIGATION_ACTION_ID_PREFIX = "__causeway_navigate_to_one_of_";
+
@Inject final BookmarkService bookmarkService;
@Inject final SudoService sudoService;
@Inject final ClockService clockService;
@@ -202,7 +206,7 @@ private Bookmark doExecuteCommand(final CommandDto dto) {
// we pass 'null' for the mixedInAdapter; if this action _is_ a mixin then
// it will switch the targetAdapter to be the mixedInAdapter transparently
- val argAdapters = argAdaptersFor(actionDto);
+ val argAdapters = argAdaptersFor(actionDto, objectAction);
val interactionHead = objectAction.interactionHead(targetAdapter);
@@ -353,6 +357,13 @@ private String argStr(Identifier actionIdentifier, int i, ParamDto paramDto) {
// name.equalsIgnoreCase("token");
// }
+ Can<ManagedObject> argAdaptersFor(final ActionDto actionDto, final ObjectAction objectAction) {
+ if(objectAction.getId().startsWith(PARENTED_COLLECTION_NAVIGATION_ACTION_ID_PREFIX)) {
+ return argAdaptersForParentedCollectionNavigation(actionDto, objectAction);
+ }
+ return argAdaptersFor(actionDto);
+ }
+
private Can<ManagedObject> argAdaptersFor(final ActionDto actionDto) {
val actionIdentifier = valueMarshaller.actionIdentifier(actionDto);
IndexedFunction<ParamDto, ManagedObject> paramDtoManagedObjectIndexedFunction = (i, paramDto) ->
@@ -361,6 +372,33 @@ private Can<ManagedObject> argAdaptersFor(final ActionDto actionDto) {
.map(IndexedFunction.zeroBased(paramDtoManagedObjectIndexedFunction)).collect(Can.toCan());
}
+ private Can<ManagedObject> argAdaptersForParentedCollectionNavigation(
+ final ActionDto actionDto,
+ final ObjectAction objectAction) {
+ // Synthetic navigate-to-one actions derive optional filters from collection columns.
+ // Older command DTOs can therefore contain fewer parameters, or parameters in a previous order.
+ // Align by stable parameter id/friendly name and pad missing filters as empty/unselected.
+ val paramDtoByName = new HashMap<String, ParamDto>();
+ val paramDtos = streamParamDtosFrom(actionDto).collect(Can.toCan());
+ paramDtos.stream()
+ .filter(paramDto -> paramDto.getName() != null)
+ .forEach(paramDto -> paramDtoByName.putIfAbsent(paramDto.getName(), paramDto));
+
+ val actionParameters = objectAction.getParameters();
+ val argAdapters = new ArrayList<ManagedObject>(actionParameters.size());
+ for(int paramNum = 0; paramNum < actionParameters.size(); paramNum++) {
+ val actionParameter = actionParameters.getElseFail(paramNum);
+ val paramDto = Optional.ofNullable(paramDtoByName.get(actionParameter.getId()))
+ .orElseGet(() -> paramDtoByName.get(actionParameter.getCanonicalFriendlyName()));
+ if(paramDto != null) {
+ argAdapters.add(valueMarshaller.recoverParameterFrom(actionParameter.getFeatureIdentifier(), paramDto));
+ continue;
+ }
+ argAdapters.add(ManagedObject.empty(actionParameter.getElementType()));
+ }
+ return Can.ofCollection(argAdapters);
+ }
+
private static Stream<ParamDto> streamParamDtosFrom(final ActionDto actionDto) {
return Optional.ofNullable(actionDto.getParameters())
.map(ParamsDto::getParameter)
diff --git a/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefaultTest.java b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefaultTest.java
new file mode 100644
index 00000000000..b238a0e0c2e
--- /dev/null
+++ b/core/runtimeservices/src/test/java/org/apache/causeway/core/runtimeservices/command/CommandExecutorServiceDefaultTest.java
@@ -0,0 +1,144 @@
+/*
+ * 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.runtimeservices.command;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.junit.jupiter.api.Test;
+
+import org.apache.causeway.applib.Identifier;
+import org.apache.causeway.applib.id.LogicalType;
+import org.apache.causeway.applib.services.bookmark.BookmarkService;
+import org.apache.causeway.applib.services.clock.ClockService;
+import org.apache.causeway.applib.services.iactnlayer.InteractionLayerTracker;
+import org.apache.causeway.applib.services.metamodel.MetaModelService;
+import org.apache.causeway.applib.services.sudo.SudoService;
+import org.apache.causeway.applib.services.xactn.TransactionService;
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.core.metamodel.object.ManagedObjects;
+import org.apache.causeway.core.metamodel.services.schema.SchemaValueMarshaller;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectActionParameter;
+import org.apache.causeway.core.metamodel.specloader.SpecificationLoader;
+import org.apache.causeway.schema.cmd.v2.ActionDto;
+import org.apache.causeway.schema.cmd.v2.ParamDto;
+import org.apache.causeway.schema.cmd.v2.ParamsDto;
+
+import lombok.val;
+
+class CommandExecutorServiceDefaultTest {
+
+ @Test
+ void pads_and_reorders_legacy_parented_collection_navigation_arguments_by_parameter_id() {
+ val fixtures = new Fixtures();
+ val service = newService(fixtures.valueMarshaller);
+ val action = action(
+ parameter("name", String.class, 0),
+ parameter("sequence", Integer.class, 1),
+ parameter("checkbox", Boolean.class, 2),
+ parameter("boundedReference", Object.class, 3),
+ parameter("choicesReference", Object.class, 4),
+ parameter("autocompleteReference", Object.class, 5));
+ val nameDto = paramDto("name");
+ val checkboxDto = paramDto("checkbox");
+ val sequenceDto = paramDto("sequence");
+ val actionDto = actionDto(checkboxDto, nameDto, sequenceDto);
+ val nameAdapter = mock(ManagedObject.class);
+ val checkboxAdapter = mock(ManagedObject.class);
+ val sequenceAdapter = mock(ManagedObject.class);
+ when(fixtures.valueMarshaller.recoverParameterFrom(eq(action.getParameters().getElseFail(0).getFeatureIdentifier()), eq(nameDto)))
+ .thenReturn(nameAdapter);
+ when(fixtures.valueMarshaller.recoverParameterFrom(eq(action.getParameters().getElseFail(1).getFeatureIdentifier()), eq(sequenceDto)))
+ .thenReturn(sequenceAdapter);
+ when(fixtures.valueMarshaller.recoverParameterFrom(eq(action.getParameters().getElseFail(2).getFeatureIdentifier()), eq(checkboxDto)))
+ .thenReturn(checkboxAdapter);
+
+ val argAdapters = service.argAdaptersFor(actionDto, action);
+
+ assertThat(argAdapters.size()).isEqualTo(6);
+ assertThat(argAdapters.getElseFail(0)).isSameAs(nameAdapter);
+ assertThat(argAdapters.getElseFail(1)).isSameAs(sequenceAdapter);
+ assertThat(argAdapters.getElseFail(2)).isSameAs(checkboxAdapter);
+ assertThat(ManagedObjects.isNullOrUnspecifiedOrEmpty(argAdapters.getElseFail(3))).isTrue();
+ assertThat(ManagedObjects.isNullOrUnspecifiedOrEmpty(argAdapters.getElseFail(4))).isTrue();
+ assertThat(ManagedObjects.isNullOrUnspecifiedOrEmpty(argAdapters.getElseFail(5))).isTrue();
+ }
+
+ private CommandExecutorServiceDefault newService(final SchemaValueMarshaller valueMarshaller) {
+ return new CommandExecutorServiceDefault(
+ mock(BookmarkService.class),
+ mock(SudoService.class),
+ mock(ClockService.class),
+ mock(TransactionService.class),
+ mock(InteractionLayerTracker.class),
+ valueMarshaller,
+ mock(MetaModelService.class),
+ mock(SpecificationLoader.class));
+ }
+
+ private ObjectAction action(final ObjectActionParameter... parameters) {
+ val action = mock(ObjectAction.class);
+ when(action.getId()).thenReturn("__causeway_navigate_to_one_of_items");
+ when(action.getParameters()).thenReturn(Can.ofArray(parameters));
+ return action;
+ }
+
+ private ObjectActionParameter parameter(
+ final String id,
+ final Class<?> type,
+ final int index) {
+ val parameter = mock(ObjectActionParameter.class);
+ val elementType = mock(ObjectSpecification.class);
+ val featureIdentifier = Identifier.actionIdentifier(
+ LogicalType.fqcn(CommandExecutorServiceDefaultTest.class),
+ "navigate",
+ new Class<?>[] { String.class, Integer.class, Boolean.class, Object.class, Object.class, Object.class })
+ .withParameterIndex(index);
+ when(elementType.getCorrespondingClass()).thenAnswer(__ -> type);
+ when(parameter.getId()).thenReturn(id);
+ when(parameter.getCanonicalFriendlyName()).thenReturn(id);
+ when(parameter.getFeatureIdentifier()).thenReturn(featureIdentifier);
+ when(parameter.getElementType()).thenReturn(elementType);
+ return parameter;
+ }
+
+ private ActionDto actionDto(final ParamDto... paramDtos) {
+ val actionDto = new ActionDto();
+ val paramsDto = new ParamsDto();
+ paramsDto.getParameter().addAll(java.util.List.of(paramDtos));
+ actionDto.setParameters(paramsDto);
+ return actionDto;
+ }
+
+ private ParamDto paramDto(final String name) {
+ val paramDto = new ParamDto();
+ paramDto.setName(name);
+ return paramDto;
+ }
+
+ private static class Fixtures {
+ private final SchemaValueMarshaller valueMarshaller = mock(SchemaValueMarshaller.class);
+ }
+
+}
(causeway) 02/03: CAUSEWAY-4038: archives navigate-to parameter OpenSpec change
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
danhaywood pushed a commit to branch CAUSEWAY-4038
in repository https://gitbox.apache.org/repos/asf/causeway.git
commit d90085d5c47b5225162d5cc5fe1266ba09962609
Author: Dan Haywood <da...@haywood-associates.co.uk>
AuthorDate: Wed Jun 17 10:20:56 2026 +0100
CAUSEWAY-4038: archives navigate-to parameter OpenSpec change
---
.../.openspec.yaml | 0
.../design.md | 0
.../proposal.md | 0
.../parented-collection-selector-actions/spec.md | 0
.../tasks.md | 0
.../parented-collection-selector-actions/spec.md | 44 ++++++++++++++++++++++
6 files changed, 44 insertions(+)
diff --git a/openspec/changes/improve-generated-navigate-to-parameters/.openspec.yaml b/openspec/changes/archive/2026-06-17-improve-generated-navigate-to-parameters/.openspec.yaml
similarity index 100%
rename from openspec/changes/improve-generated-navigate-to-parameters/.openspec.yaml
rename to openspec/changes/archive/2026-06-17-improve-generated-navigate-to-parameters/.openspec.yaml
diff --git a/openspec/changes/improve-generated-navigate-to-parameters/design.md b/openspec/changes/archive/2026-06-17-improve-generated-navigate-to-parameters/design.md
similarity index 100%
rename from openspec/changes/improve-generated-navigate-to-parameters/design.md
rename to openspec/changes/archive/2026-06-17-improve-generated-navigate-to-parameters/design.md
diff --git a/openspec/changes/improve-generated-navigate-to-parameters/proposal.md b/openspec/changes/archive/2026-06-17-improve-generated-navigate-to-parameters/proposal.md
similarity index 100%
rename from openspec/changes/improve-generated-navigate-to-parameters/proposal.md
rename to openspec/changes/archive/2026-06-17-improve-generated-navigate-to-parameters/proposal.md
diff --git a/openspec/changes/improve-generated-navigate-to-parameters/specs/parented-collection-selector-actions/spec.md b/openspec/changes/archive/2026-06-17-improve-generated-navigate-to-parameters/specs/parented-collection-selector-actions/spec.md
similarity index 100%
rename from openspec/changes/improve-generated-navigate-to-parameters/specs/parented-collection-selector-actions/spec.md
rename to openspec/changes/archive/2026-06-17-improve-generated-navigate-to-parameters/specs/parented-collection-selector-actions/spec.md
diff --git a/openspec/changes/improve-generated-navigate-to-parameters/tasks.md b/openspec/changes/archive/2026-06-17-improve-generated-navigate-to-parameters/tasks.md
similarity index 100%
rename from openspec/changes/improve-generated-navigate-to-parameters/tasks.md
rename to openspec/changes/archive/2026-06-17-improve-generated-navigate-to-parameters/tasks.md
diff --git a/openspec/specs/parented-collection-selector-actions/spec.md b/openspec/specs/parented-collection-selector-actions/spec.md
index 4b84b3f82ba..3f59d05c457 100644
--- a/openspec/specs/parented-collection-selector-actions/spec.md
+++ b/openspec/specs/parented-collection-selector-actions/spec.md
@@ -450,3 +450,47 @@ The system SHALL avoid adding synthetic scalar reference navigate-to actions to
- **WHEN** the recorder enumerates actions for navigating a scalar reference
- **THEN** the synthetic navigate-to action is available for recording the reference traversal step
+### Requirement: Selector action boolean filters are optional tri-valued predicates
+The synthetic selector action SHALL define each generated boolean child-column filter parameter as optional even when the underlying child property is mandatory.
+The generated boolean filter parameter SHALL support three caller states: unselected, explicitly `true`, and explicitly `false`.
+The synthetic selector action MUST NOT apply a boolean predicate when the generated boolean filter parameter is unselected, null, unspecified, or empty.
+The synthetic selector action SHALL apply an exact boolean predicate when the generated boolean filter parameter is explicitly `true` or explicitly `false`.
+The synthetic selector action SHALL keep non-boolean scalar and selectable reference filter semantics unchanged.
+
+#### Scenario: Mandatory checkbox column becomes optional generated filter
+- **GIVEN** `Lease.items` renders a mandatory boolean child property as a collection column
+- **WHEN** the framework synthesizes the selector action for `Lease.items`
+- **THEN** the action has an optional filter parameter corresponding to the mandatory boolean column
+- **AND** the parameter can be left unselected by the caller
+
+#### Scenario: Unselected boolean filter is not applied
+- **GIVEN** a parented collection contains child objects with both `true` and `false` values for a mandatory boolean column
+- **WHEN** the synthetic selector action validates or invokes with the generated boolean filter unselected
+- **THEN** the boolean column does not constrain the matching child objects
+
+#### Scenario: Explicit false boolean filter is applied
+- **GIVEN** a parented collection contains child objects with both `true` and `false` values for a mandatory boolean column
+- **WHEN** the synthetic selector action validates or invokes with the generated boolean filter explicitly set to `false`
+- **THEN** only child objects whose boolean column value is `false` match that filter
+
+#### Scenario: Explicit true boolean filter is applied
+- **GIVEN** a parented collection contains child objects with both `true` and `false` values for a mandatory boolean column
+- **WHEN** the synthetic selector action validates or invokes with the generated boolean filter explicitly set to `true`
+- **THEN** only child objects whose boolean column value is `true` match that filter
+
+### Requirement: Selector action parameter prompts follow parented collection column order
+The synthetic selector action SHALL order generated child filter parameters by the associated parented collection column order before omitting ineligible columns.
+The synthetic selector action SHALL preserve the relative order of eligible generated parameters when ineligible columns appear between them.
+The synthetic selector action MUST NOT fall back to child property declaration order when explicit parented collection column order metadata is available.
+
+#### Scenario: Eligible parameters follow configured collection column order
+- **GIVEN** `Lease.items` renders eligible child columns in the order `checkbox`, `name`, and `sequence`
+- **WHEN** the framework synthesizes the selector action for `Lease.items`
+- **THEN** the generated filter parameters appear in the order `checkbox`, `name`, and `sequence`
+
+#### Scenario: Ineligible columns are skipped without reordering eligible parameters
+- **GIVEN** `Lease.items` renders child columns in the order `name`, `attachment`, `checkbox`, and `sequence`
+- **AND** `attachment` is not eligible as a generated selector filter parameter
+- **WHEN** the framework synthesizes the selector action for `Lease.items`
+- **THEN** the generated filter parameters appear in the order `name`, `checkbox`, and `sequence`
+