propertyFiltersPredicate;
protected VerticalLayout contentWrapper;
@@ -362,6 +368,11 @@ public DataLoader getDataLoader() {
/**
* Sets a {@link DataLoader} related to the filter.
+ *
+ * The data loader's own condition is preserved and combined (AND) with the current
+ * configuration's condition. It is captured once, before the filter first applies a
+ * configuration; therefore any base loader condition must be set (in XML or in {@code onInit})
+ * before a configuration is activated.
*
* @param dataLoader a {@link DataLoader} to set
*/
@@ -370,15 +381,21 @@ public void setDataLoader(DataLoader dataLoader) {
checkNotNull(dataLoader);
this.dataLoader = dataLoader;
- this.initialDataLoaderCondition = dataLoader.getCondition();
LogicalFilterComponent> rootLogicalFilterComponent = emptyConfiguration.getRootLogicalFilterComponent();
rootLogicalFilterComponent.setDataLoader(dataLoader);
rootLogicalFilterComponent.setAutoApply(autoApply);
}
+ /**
+ * @deprecated no longer used internally; the initial data loader condition is now captured lazily
+ * in {@link #updateDataLoaderCondition()} before the first filter contribution. Retained for
+ * backward compatibility.
+ */
+ @Deprecated
protected void updateDataLoaderInitialCondition(@Nullable Condition condition) {
this.initialDataLoaderCondition = copy(condition);
+ this.initialDataLoaderConditionInitialized = true;
}
/**
@@ -830,6 +847,12 @@ protected String getConfigurationName(Configuration configuration) {
protected void updateDataLoaderCondition() {
if (dataLoader != null) {
+ if (!initialDataLoaderConditionInitialized) {
+ // Capture the data loader's own condition once, before the first filter contribution,
+ // so it is never polluted by a configuration activated during onInit.
+ initialDataLoaderCondition = copy(dataLoader.getCondition());
+ initialDataLoaderConditionInitialized = true;
+ }
LogicalFilterComponent> logicalFilterComponent = getCurrentConfiguration().getRootLogicalFilterComponent();
LogicalCondition filterCondition = logicalFilterComponent.getQueryCondition();
diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilter.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilter.java
index 8372409ce1..c425a22e93 100644
--- a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilter.java
+++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilter.java
@@ -65,6 +65,7 @@ public class GroupFilter extends Composite
protected DataLoader dataLoader;
protected Condition initialDataLoaderCondition;
+ protected boolean initialDataLoaderConditionInitialized;
protected boolean autoApply;
@Internal
@@ -186,7 +187,6 @@ public void setDataLoader(DataLoader dataLoader) {
checkNotNull(dataLoader);
this.dataLoader = dataLoader;
- this.initialDataLoaderCondition = dataLoader.getCondition();
if (!isConditionModificationDelegated()) {
updateDataLoaderCondition();
@@ -195,8 +195,15 @@ public void setDataLoader(DataLoader dataLoader) {
updateSummaryText();
}
+ /**
+ * @deprecated no longer used internally; the initial data loader condition is now captured lazily
+ * in {@link #updateDataLoaderCondition()} before the first filter contribution. Retained for
+ * backward compatibility.
+ */
+ @Deprecated
protected void updateDataLoaderInitialCondition(@Nullable Condition condition) {
this.initialDataLoaderCondition = copy(condition);
+ this.initialDataLoaderConditionInitialized = true;
}
protected void updateDataLoaderCondition() {
@@ -204,6 +211,12 @@ protected void updateDataLoaderCondition() {
return;
}
+ if (!initialDataLoaderConditionInitialized) {
+ // Capture the data loader's own condition once, before the first filter contribution.
+ initialDataLoaderCondition = copy(dataLoader.getCondition());
+ initialDataLoaderConditionInitialized = true;
+ }
+
LogicalCondition resultCondition;
if (initialDataLoaderCondition instanceof LogicalCondition initialLogicalCondition) {
resultCondition = ((LogicalCondition) copy(initialLogicalCondition));
diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilterUtils.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilterUtils.java
index 6c7fc4867a..36d3002b3a 100644
--- a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilterUtils.java
+++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilterUtils.java
@@ -23,6 +23,11 @@
@Internal
public class GroupFilterUtils {
+ /**
+ * @deprecated no longer used internally; {@link GroupFilter} now captures the initial data loader
+ * condition lazily. Retained for backward compatibility.
+ */
+ @Deprecated
public static void updateDataLoaderInitialCondition(GroupFilter groupFilter, @Nullable Condition condition) {
groupFilter.updateDataLoaderInitialCondition(condition);
}
diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/xml/layout/loader/component/GenericFilterLoader.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/xml/layout/loader/component/GenericFilterLoader.java
index f171f8f54a..b70a3c3521 100644
--- a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/xml/layout/loader/component/GenericFilterLoader.java
+++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/xml/layout/loader/component/GenericFilterLoader.java
@@ -87,14 +87,6 @@ protected void loadDataLoader(GenericFilter component, Element element) {
(dataLoaderId) -> {
DataLoader dataLoader = getContext().getDataHolder().getLoader(dataLoaderId);
component.setDataLoader(dataLoader);
-
- getContext().addInitTask(new AbstractInitTask() {
- @Override
- public void execute(Context context) {
- FilterUtils.updateDataLoaderInitialCondition(resultComponent,
- dataLoader.getCondition());
- }
- });
});
}
diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/xml/layout/loader/component/GroupFilterLoader.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/xml/layout/loader/component/GroupFilterLoader.java
index a004a3cc44..efb54ee7e6 100644
--- a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/xml/layout/loader/component/GroupFilterLoader.java
+++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/xml/layout/loader/component/GroupFilterLoader.java
@@ -18,11 +18,9 @@
import io.jmix.flowui.component.filter.FilterComponent;
import io.jmix.flowui.component.logicalfilter.GroupFilter;
-import io.jmix.flowui.component.logicalfilter.GroupFilterUtils;
import io.jmix.flowui.component.logicalfilter.LogicalFilterComponent;
import io.jmix.flowui.model.DataLoader;
import io.jmix.flowui.xml.layout.ComponentLoader;
-import io.jmix.flowui.xml.layout.inittask.AbstractInitTask;
import io.jmix.flowui.xml.layout.loader.AbstractComponentLoader;
import org.dom4j.Element;
@@ -55,14 +53,6 @@ protected void loadDataLoader(GroupFilter resultComponent, Element element) {
.ifPresent(dataLoaderId -> {
DataLoader dataLoader = context.getDataHolder().getLoader(dataLoaderId);
resultComponent.setDataLoader(dataLoader);
-
- getContext().addInitTask(new AbstractInitTask() {
- @Override
- public void execute(Context context) {
- GroupFilterUtils.updateDataLoaderInitialCondition(resultComponent,
- dataLoader.getCondition());
- }
- });
});
}
From a3ea99c401726e8dca2b30ce310e1b14276d35d3 Mon Sep 17 00:00:00 2001
From: aleksandrovpv
Date: Wed, 24 Jun 2026 16:50:01 +0400
Subject: [PATCH 2/3] GenericFilter: tests for configuration activation in
onInit jmix-framework/jmix#5400
Data-level integration tests (seeded rows, asserting the loaded data, not just the condition object):
- switching configurations applies only the target configuration's condition;
- with a DataLoadCoordinator the initial load is filtered;
- plain setCurrentConfiguration in onInit no longer pollutes the baseline;
- ResetAction (#2406) no-accumulation guard (repeated apply/switch stays correct).
Also covers builder activation phases and extends the builder API tests.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../GenericFilterBuilderApiTest.groovy | 84 ++++++++++++
.../GenericFilterFilteredDataTest.groovy | 121 ++++++++++++++++++
.../GenericFilterInitialConditionTest.groovy | 62 +++++++++
...ericFilterMakeCurrentActivationTest.groovy | 115 +++++++++++++++++
...GenericFilterInitialConditionTestView.java | 59 +++++++++
.../view/GfActivationBeforeShowView.java | 66 ++++++++++
.../view/GfActivationDoubleLoadView.java | 74 +++++++++++
.../view/GfActivationOnInitDlcView.java | 69 ++++++++++
.../view/GfActivationOnInitNoDlcView.java | 74 +++++++++++
.../view/GfActivationPlainSetCurrentView.java | 66 ++++++++++
.../view/GfDlcFilteredLoadTestView.java | 62 +++++++++
.../view/GfDlcPlainSetCurrentTestView.java | 65 ++++++++++
...ric-filter-initial-condition-test-view.xml | 32 +++++
.../view/gf-activation-dlc-view.xml | 35 +++++
.../view/gf-activation-nodlc-view.xml | 32 +++++
15 files changed, 1016 insertions(+)
create mode 100644 jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterFilteredDataTest.groovy
create mode 100644 jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterInitialConditionTest.groovy
create mode 100644 jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterMakeCurrentActivationTest.groovy
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GenericFilterInitialConditionTestView.java
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationBeforeShowView.java
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationDoubleLoadView.java
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationOnInitDlcView.java
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationOnInitNoDlcView.java
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationPlainSetCurrentView.java
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfDlcFilteredLoadTestView.java
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfDlcPlainSetCurrentTestView.java
create mode 100644 jmix-flowui/flowui/src/test/resources/component/genericfilter/view/generic-filter-initial-condition-test-view.xml
create mode 100644 jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-activation-dlc-view.xml
create mode 100644 jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-activation-nodlc-view.xml
diff --git a/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBuilderApiTest.groovy b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBuilderApiTest.groovy
index 6e6d493a8e..71716ce950 100644
--- a/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBuilderApiTest.groovy
+++ b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBuilderApiTest.groovy
@@ -853,4 +853,88 @@ class GenericFilterBuilderApiTest extends FlowuiTestSpecification {
then:
noExceptionThrown()
}
+
+ // Builder options and RunTimeConfigurationBuilder edge branches
+
+ def "PropertyFilterBuilder.label() sets the label on the built PropertyFilter"() {
+ given:
+ GenericFilter filter = filterWithLoader()
+
+ when:
+ PropertyFilter pf = filter.filterComponentBuilder()
+ . propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .label("Order number")
+ .build()
+
+ then:
+ pf.label == "Order number"
+ }
+
+ def "JpqlFilterBuilder.join() sets the JOIN clause on the built JpqlFilter"() {
+ given:
+ GenericFilter filter = filterWithLoader()
+
+ when:
+ JpqlFilter jf = filter.filterComponentBuilder()
+ .jpqlFilter(String)
+ .parameterName("tag")
+ .where("t.name = ?")
+ .join("join {E}.tags t")
+ .build()
+
+ then:
+ jf.queryCondition.join == "join {E}.tags t"
+ }
+
+ def "RunTimeConfigurationBuilder.buildAndRegister() throws when the filter has no DataLoader"() {
+ given: "a GenericFilter without a DataLoader, with an id set so the DataLoader check is reached"
+ GenericFilter filter = uiComponents.create(GenericFilter)
+
+ when:
+ filter.runtimeConfigurationBuilder()
+ .id("noLoader")
+ .buildAndRegister()
+
+ then:
+ thrown(IllegalStateException)
+ }
+
+ def "RunTimeConfigurationBuilder.add() accepts a non-single filter component (GroupFilter)"() {
+ given: "a GenericFilter with a DataLoader and a GroupFilter condition"
+ GenericFilter filter = filterWithLoader()
+ GroupFilter group = filter.filterComponentBuilder()
+ .groupFilter()
+ .add(filter.filterComponentBuilder().jpqlFilter().where("{E}.number = '1'").build())
+ .build()
+
+ when: "adding the GroupFilter (not a SingleFilterComponentBase) to a runtime configuration"
+ RunTimeConfiguration config = filter.runtimeConfigurationBuilder()
+ .id("withGroup")
+ .add(group)
+ .buildAndRegister()
+
+ then: "the GroupFilter is part of the configuration"
+ config.rootLogicalFilterComponent.filterComponents.contains(group)
+ }
+
+ def "RunTimeConfigurationBuilder.add() of a value-less single component stores no default value"() {
+ given: "a GenericFilter and a PropertyFilter on a numeric property with no value"
+ GenericFilter filter = filterWithLoader()
+ PropertyFilter pf = filter.filterComponentBuilder()
+ . propertyFilter()
+ .property("amount")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build()
+
+ when: "adding it without a value (paramName non-null, value null)"
+ RunTimeConfiguration config = filter.runtimeConfigurationBuilder()
+ .id("noValue")
+ .add(pf)
+ .buildAndRegister()
+
+ then: "no default value is recorded for the parameter"
+ config.getFilterComponentDefaultValue(pf.parameterName) == null
+ }
}
diff --git a/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterFilteredDataTest.groovy b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterFilteredDataTest.groovy
new file mode 100644
index 0000000000..fa7521f99e
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterFilteredDataTest.groovy
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter
+
+import component.genericfilter.view.GfDlcFilteredLoadTestView
+import component.genericfilter.view.GfDlcPlainSetCurrentTestView
+import io.jmix.core.DataManager
+import io.jmix.flowui.component.genericfilter.GenericFilter
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import test_support.entity.sales.Order
+import test_support.spec.FlowuiTestSpecification
+
+/**
+ * Data-level integration tests: they seed real {@code Order} rows and assert the rows actually
+ * loaded into the data container (not just the loader's condition object), with a
+ * {@code DataLoadCoordinator} present.
+ *
+ * Seed: 3 orders with number = FLT_MATCH, 2 with number = FLT_OTHER.
+ */
+@SpringBootTest
+class GenericFilterFilteredDataTest extends FlowuiTestSpecification {
+
+ @Autowired
+ DataManager dataManager
+
+ List seeded = []
+
+ void setup() {
+ registerViewBasePackages("component.genericfilter.view")
+ seeded = [
+ dataManager.save(new Order(number: "FLT_MATCH")),
+ dataManager.save(new Order(number: "FLT_MATCH")),
+ dataManager.save(new Order(number: "FLT_MATCH")),
+ dataManager.save(new Order(number: "FLT_OTHER")),
+ dataManager.save(new Order(number: "FLT_OTHER")),
+ ]
+ }
+
+ void cleanup() {
+ seeded.each { dataManager.remove(it) }
+ }
+
+ def "with a DataLoadCoordinator, the initial grid shows only the active configuration's rows"() {
+ when: "the view opens; configuration 'match' (number = FLT_MATCH) is active via makeCurrent() in onInit"
+ GenericFilter filter = navigateToView(GfDlcFilteredLoadTestView).genericFilter
+ def items = filter.dataLoader.container.items
+
+ then: "only the 3 matching orders are loaded, not all seeded orders"
+ items.size() == 3
+ items.every { it.number == "FLT_MATCH" }
+ }
+
+ def "switching configuration loads only the target configuration's rows"() {
+ given:
+ GenericFilter filter = navigateToView(GfDlcFilteredLoadTestView).genericFilter
+
+ when: "switching to configuration 'other' (number = FLT_OTHER) and applying"
+ filter.setCurrentConfiguration(filter.getConfiguration("other"))
+ filter.apply()
+ def items = filter.dataLoader.container.items
+
+ then: "only the 2 'other' orders are loaded (not 0 from condition stacking)"
+ items.size() == 2
+ items.every { it.number == "FLT_OTHER" }
+ }
+
+ def "plain setCurrentConfiguration in onInit: initial load filtered and switching loads only the target rows"() {
+ when: "the view opens; 'match' is activated via base-API setCurrentConfiguration in onInit (no builder)"
+ GenericFilter filter = navigateToView(GfDlcPlainSetCurrentTestView).genericFilter
+
+ then: "initial load is filtered to the 3 matching rows"
+ filter.dataLoader.container.items.size() == 3
+ filter.dataLoader.container.items.every { it.number == "FLT_MATCH" }
+
+ when: "switching to 'other'"
+ filter.setCurrentConfiguration(filter.getConfiguration("other"))
+ filter.apply()
+
+ then: "only the 2 'other' rows are loaded (not 0 from condition stacking)"
+ filter.dataLoader.container.items.size() == 2
+ filter.dataLoader.container.items.every { it.number == "FLT_OTHER" }
+ }
+
+ def "repeated configuration switching does not accumulate conditions (issue #2406 guard)"() {
+ given:
+ GenericFilter filter = navigateToView(GfDlcFilteredLoadTestView).genericFilter
+
+ when: "switching back and forth several times, applying each time"
+ filter.setCurrentConfiguration(filter.getConfiguration("other"))
+ filter.apply()
+ def afterOther1 = filter.dataLoader.container.items.collect { it.number }
+
+ filter.setCurrentConfiguration(filter.getConfiguration("match"))
+ filter.apply()
+ def afterMatch = filter.dataLoader.container.items.collect { it.number }
+
+ filter.setCurrentConfiguration(filter.getConfiguration("other"))
+ filter.apply()
+ def afterOther2 = filter.dataLoader.container.items.collect { it.number }
+
+ then: "each switch yields exactly the target rows — no accumulation/corruption across applies"
+ afterOther1.size() == 2 && afterOther1.every { it == "FLT_OTHER" }
+ afterMatch.size() == 3 && afterMatch.every { it == "FLT_MATCH" }
+ afterOther2.size() == 2 && afterOther2.every { it == "FLT_OTHER" }
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterInitialConditionTest.groovy b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterInitialConditionTest.groovy
new file mode 100644
index 0000000000..87b4b73954
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterInitialConditionTest.groovy
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter
+
+import component.genericfilter.view.GenericFilterInitialConditionTestView
+import io.jmix.core.querycondition.Condition
+import io.jmix.core.querycondition.LogicalCondition
+import io.jmix.core.querycondition.PropertyCondition
+import io.jmix.flowui.component.genericfilter.GenericFilter
+import org.springframework.boot.test.context.SpringBootTest
+import test_support.spec.FlowuiTestSpecification
+
+/**
+ * Regression test: activating a configuration during {@code onInit} must not pollute the data
+ * loader's initial condition, so switching to another configuration applies only that
+ * configuration's condition (not the previously active one ANDed on top).
+ */
+@SpringBootTest
+class GenericFilterInitialConditionTest extends FlowuiTestSpecification {
+
+ void setup() {
+ registerViewBasePackages("component.genericfilter.view")
+ }
+
+ def "switching configuration applies only its own condition when a configuration was activated in onInit"() {
+ when: "the view opens with configuration 'c1' activated in onInit via makeCurrent()"
+ GenericFilter filter = navigateToView(GenericFilterInitialConditionTestView).genericFilter
+
+ then: "the data loader condition reflects only 'c1'"
+ countPropertyConditions(filter.dataLoader.condition) == 1
+
+ when: "switching to configuration 'c2'"
+ filter.setCurrentConfiguration(filter.getConfiguration("c2"))
+
+ then: "the data loader condition reflects only 'c2', not 'c1' AND 'c2'"
+ countPropertyConditions(filter.dataLoader.condition) == 1
+ }
+
+ protected static int countPropertyConditions(Condition condition) {
+ if (condition instanceof PropertyCondition) {
+ return 1
+ }
+ if (condition instanceof LogicalCondition) {
+ return condition.conditions.inject(0) { acc, c -> acc + countPropertyConditions(c) }
+ }
+ return 0
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterMakeCurrentActivationTest.groovy b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterMakeCurrentActivationTest.groovy
new file mode 100644
index 0000000000..00e8fbd52d
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterMakeCurrentActivationTest.groovy
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter
+
+import component.genericfilter.view.GfActivationBeforeShowView
+import component.genericfilter.view.GfActivationDoubleLoadView
+import component.genericfilter.view.GfActivationOnInitDlcView
+import component.genericfilter.view.GfActivationOnInitNoDlcView
+import component.genericfilter.view.GfActivationPlainSetCurrentView
+import io.jmix.core.querycondition.Condition
+import io.jmix.core.querycondition.LogicalCondition
+import io.jmix.core.querycondition.PropertyCondition
+import org.springframework.boot.test.context.SpringBootTest
+import test_support.spec.FlowuiTestSpecification
+
+/**
+ * Integration tests for the variability of configuration activation via the GenericFilter builder:
+ * the lifecycle phase in which a configuration is made current, whether a {@code DataLoadCoordinator}
+ * is present, and the resulting load behavior. Complements {@code GenericFilterInitialConditionTest},
+ * which covers the core "switching does not stack conditions" invariant for the {@code onInit} path.
+ */
+@SpringBootTest
+class GenericFilterMakeCurrentActivationTest extends FlowuiTestSpecification {
+
+ void setup() {
+ registerViewBasePackages("component.genericfilter.view")
+ }
+
+ def "makeCurrent in onInit activates the configuration synchronously"() {
+ when: "the view opens; c1 is made current via makeCurrent() in onInit"
+ def view = navigateToView(GfActivationOnInitNoDlcView)
+
+ then: "the configuration was made current immediately during onInit (right after buildAndRegister)"
+ view.currentRightAfterMakeCurrent.id == "c1"
+
+ and: "it remains current after the view is shown"
+ view.genericFilter.currentConfiguration.id == "c1"
+ }
+
+ def "without a DataLoadCoordinator, makeCurrent triggers a single filtered load on open"() {
+ when: "the view opens (no DataLoadCoordinator); applyFilterIfNeeded loads the default-valued config"
+ def view = navigateToView(GfActivationOnInitNoDlcView)
+
+ then: "exactly one load happened, filtered by the active configuration"
+ view.loadCount == 1
+ countPropertyConditions(view.genericFilter.dataLoader.condition) == 1
+ }
+
+ def "with a DataLoadCoordinator, makeCurrent yields exactly one filtered load on open"() {
+ when: "the view opens (with a DataLoadCoordinator)"
+ def view = navigateToView(GfActivationOnInitDlcView)
+
+ then: "exactly one load happened, with the configuration's condition applied"
+ view.loadCount == 1
+ countPropertyConditions(view.genericFilter.dataLoader.condition) == 1
+ }
+
+ def "activation in BeforeShow (filter already attached) is synchronous and switching applies only the new configuration"() {
+ when: "the view opens; c1 is made current via makeCurrent() in BeforeShow"
+ def view = navigateToView(GfActivationBeforeShowView)
+
+ then: "c1 is current and only its condition is applied"
+ view.genericFilter.currentConfiguration.id == "c1"
+ countPropertyConditions(view.genericFilter.dataLoader.condition) == 1
+
+ when: "switching to c2"
+ view.switchTo("c2")
+
+ then: "only c2's condition is applied, not c1 AND c2"
+ countPropertyConditions(view.genericFilter.dataLoader.condition) == 1
+ }
+
+ def "a current default configuration plus a deferred makeCurrent triggers exactly one load (no double load)"() {
+ when: "the view opens: a default-like config is current in onInit, another is makeCurrent (deferred), no DataLoadCoordinator"
+ def view = navigateToView(GfActivationDoubleLoadView)
+
+ then: "exactly one load happened — the deferred activation did not add a second one"
+ view.loadCount == 1
+ }
+
+ def "plain setCurrentConfiguration in onInit does not pollute the baseline"() {
+ given: "the view opens; c1 is activated via base-API setCurrentConfiguration in onInit"
+ def view = navigateToView(GfActivationPlainSetCurrentView)
+
+ when: "switching to c2"
+ view.genericFilter.setCurrentConfiguration(view.genericFilter.getConfiguration("c2"))
+
+ then: "only c2's condition is applied"
+ countPropertyConditions(view.genericFilter.dataLoader.condition) == 1
+ }
+
+ protected static int countPropertyConditions(Condition condition) {
+ if (condition instanceof PropertyCondition) {
+ return 1
+ }
+ if (condition instanceof LogicalCondition) {
+ return condition.conditions.inject(0) { acc, c -> acc + countPropertyConditions(c) }
+ }
+ return 0
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GenericFilterInitialConditionTestView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GenericFilterInitialConditionTestView.java
new file mode 100644
index 0000000000..d5e107c9a4
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GenericFilterInitialConditionTestView.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.flowui.component.genericfilter.GenericFilter;
+import io.jmix.flowui.component.propertyfilter.PropertyFilter;
+import io.jmix.flowui.view.*;
+
+@Route(value = "generic-filter-initial-condition-test-view")
+@ViewController("GenericFilterInitialConditionTestView")
+@ViewDescriptor("generic-filter-initial-condition-test-view.xml")
+public class GenericFilterInitialConditionTestView extends StandardView {
+
+ @ViewComponent
+ public GenericFilter genericFilter;
+
+ @Subscribe
+ public void onInit(final InitEvent event) {
+ // Two configurations on the same property; the first is activated during onInit,
+ // i.e. before the loader's init tasks run.
+ PropertyFilter number1 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c1")
+ .name("C1")
+ .add(number1, "n1")
+ .makeCurrent()
+ .buildAndRegister();
+
+ PropertyFilter number2 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c2")
+ .name("C2")
+ .add(number2, "n2")
+ .buildAndRegister();
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationBeforeShowView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationBeforeShowView.java
new file mode 100644
index 0000000000..73aa38d9d3
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationBeforeShowView.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.flowui.component.genericfilter.GenericFilter;
+import io.jmix.flowui.component.propertyfilter.PropertyFilter;
+import io.jmix.flowui.view.*;
+
+/**
+ * Activates a configuration via {@code makeCurrent()} in {@code BeforeShowEvent}, i.e. when the filter
+ * is already attached — the synchronous activation path. Switching afterwards must apply only the new
+ * configuration's condition (the baseline was captured before, so it stays clean).
+ */
+@Route(value = "gf-activation-beforeshow-view")
+@ViewController("GfActivationBeforeShowView")
+@ViewDescriptor("gf-activation-nodlc-view.xml")
+public class GfActivationBeforeShowView extends StandardView {
+
+ @ViewComponent
+ public GenericFilter genericFilter;
+
+ @Subscribe
+ public void onBeforeShow(final BeforeShowEvent event) {
+ PropertyFilter number1 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c1")
+ .name("C1")
+ .add(number1, "n1")
+ .makeCurrent()
+ .buildAndRegister();
+
+ PropertyFilter number2 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c2")
+ .name("C2")
+ .add(number2, "n2")
+ .buildAndRegister();
+ }
+
+ public void switchTo(String configurationId) {
+ genericFilter.setCurrentConfiguration(genericFilter.getConfiguration(configurationId));
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationDoubleLoadView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationDoubleLoadView.java
new file mode 100644
index 0000000000..c629f0a38e
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationDoubleLoadView.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.flowui.component.genericfilter.GenericFilter;
+import io.jmix.flowui.component.genericfilter.configuration.RunTimeConfiguration;
+import io.jmix.flowui.component.propertyfilter.PropertyFilter;
+import io.jmix.flowui.model.CollectionLoader;
+import io.jmix.flowui.view.*;
+import test_support.entity.sales.Order;
+
+/**
+ * No {@code DataLoadCoordinator}. A configuration with a value is made current synchronously in
+ * {@code onInit} (so {@code applyFilterIfNeeded} loads it once), and a second configuration is
+ * activated via the builder's deferred {@code makeCurrent()}. The deferred activation must NOT add a
+ * second load — exactly one load is expected on open.
+ */
+@Route(value = "gf-activation-double-load-view")
+@ViewController("GfActivationDoubleLoadView")
+@ViewDescriptor("gf-activation-nodlc-view.xml")
+public class GfActivationDoubleLoadView extends StandardView {
+
+ @ViewComponent
+ public GenericFilter genericFilter;
+ @ViewComponent
+ private CollectionLoader ordersDl;
+
+ public int loadCount;
+
+ @Subscribe
+ public void onInit(final InitEvent event) {
+ ordersDl.addPostLoadListener(e -> loadCount++);
+
+ PropertyFilter declValue = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ RunTimeConfiguration declConfiguration = genericFilter.runtimeConfigurationBuilder()
+ .id("decl")
+ .name("Declarative-like default")
+ .add(declValue, "d1")
+ .buildAndRegister();
+ // Make it current synchronously, emulating a default configuration that applyFilterIfNeeded loads.
+ genericFilter.setCurrentConfiguration(declConfiguration);
+
+ PropertyFilter runtimeValue = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("rt")
+ .name("Runtime")
+ .add(runtimeValue, "n1")
+ .makeCurrent()
+ .buildAndRegister();
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationOnInitDlcView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationOnInitDlcView.java
new file mode 100644
index 0000000000..c83d0ca242
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationOnInitDlcView.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.flowui.component.genericfilter.GenericFilter;
+import io.jmix.flowui.component.propertyfilter.PropertyFilter;
+import io.jmix.flowui.model.CollectionLoader;
+import io.jmix.flowui.view.*;
+import test_support.entity.sales.Order;
+
+/**
+ * Same as {@link GfActivationOnInitNoDlcView} but WITH a {@code DataLoadCoordinator}: the configuration
+ * activated via {@code makeCurrent()} in {@code onInit} must yield exactly one filtered load on open.
+ */
+@Route(value = "gf-activation-oninit-dlc-view")
+@ViewController("GfActivationOnInitDlcView")
+@ViewDescriptor("gf-activation-dlc-view.xml")
+public class GfActivationOnInitDlcView extends StandardView {
+
+ @ViewComponent
+ public GenericFilter genericFilter;
+ @ViewComponent
+ private CollectionLoader ordersDl;
+
+ public int loadCount;
+
+ @Subscribe
+ public void onInit(final InitEvent event) {
+ ordersDl.addPostLoadListener(e -> loadCount++);
+
+ PropertyFilter number1 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c1")
+ .name("C1")
+ .add(number1, "n1")
+ .makeCurrent()
+ .buildAndRegister();
+
+ PropertyFilter number2 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c2")
+ .name("C2")
+ .add(number2, "n2")
+ .buildAndRegister();
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationOnInitNoDlcView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationOnInitNoDlcView.java
new file mode 100644
index 0000000000..676b3e6cec
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationOnInitNoDlcView.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.flowui.component.genericfilter.Configuration;
+import io.jmix.flowui.component.genericfilter.GenericFilter;
+import io.jmix.flowui.component.propertyfilter.PropertyFilter;
+import io.jmix.flowui.model.CollectionLoader;
+import io.jmix.flowui.view.*;
+import test_support.entity.sales.Order;
+
+/**
+ * No {@code DataLoadCoordinator}. Two configurations on {@code number}; the first is activated via the
+ * builder's {@code makeCurrent()} during {@code onInit} (deferred-to-attach path).
+ */
+@Route(value = "gf-activation-oninit-nodlc-view")
+@ViewController("GfActivationOnInitNoDlcView")
+@ViewDescriptor("gf-activation-nodlc-view.xml")
+public class GfActivationOnInitNoDlcView extends StandardView {
+
+ @ViewComponent
+ public GenericFilter genericFilter;
+ @ViewComponent
+ private CollectionLoader ordersDl;
+
+ public int loadCount;
+ public Configuration currentRightAfterMakeCurrent;
+
+ @Subscribe
+ public void onInit(final InitEvent event) {
+ ordersDl.addPostLoadListener(e -> loadCount++);
+
+ PropertyFilter number1 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c1")
+ .name("C1")
+ .add(number1, "n1")
+ .makeCurrent()
+ .buildAndRegister();
+
+ // Captured during onInit: activation is deferred, so this must still be the empty configuration.
+ currentRightAfterMakeCurrent = genericFilter.getCurrentConfiguration();
+
+ PropertyFilter number2 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c2")
+ .name("C2")
+ .add(number2, "n2")
+ .buildAndRegister();
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationPlainSetCurrentView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationPlainSetCurrentView.java
new file mode 100644
index 0000000000..be61432f80
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfActivationPlainSetCurrentView.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.flowui.component.genericfilter.GenericFilter;
+import io.jmix.flowui.component.genericfilter.configuration.RunTimeConfiguration;
+import io.jmix.flowui.component.propertyfilter.PropertyFilter;
+import io.jmix.flowui.view.*;
+
+/**
+ * Activates a configuration via the base-API {@code setCurrentConfiguration()} (NOT the builder's
+ * deferred {@code makeCurrent()}) during {@code onInit}. This is the latent base-API issue: the
+ * initial-condition baseline is polluted, so switching configurations later stacks them. The
+ * corresponding test is {@code @PendingFeature} until the core hardening lands.
+ */
+@Route(value = "gf-activation-plain-setcurrent-view")
+@ViewController("GfActivationPlainSetCurrentView")
+@ViewDescriptor("gf-activation-nodlc-view.xml")
+public class GfActivationPlainSetCurrentView extends StandardView {
+
+ @ViewComponent
+ public GenericFilter genericFilter;
+
+ @Subscribe
+ public void onInit(final InitEvent event) {
+ PropertyFilter number1 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ RunTimeConfiguration c1 = genericFilter.runtimeConfigurationBuilder()
+ .id("c1")
+ .name("C1")
+ .add(number1, "n1")
+ .buildAndRegister();
+
+ PropertyFilter number2 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c2")
+ .name("C2")
+ .add(number2, "n2")
+ .buildAndRegister();
+
+ // Base-API activation during onInit (no builder makeCurrent): pollutes the baseline.
+ genericFilter.setCurrentConfiguration(c1);
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfDlcFilteredLoadTestView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfDlcFilteredLoadTestView.java
new file mode 100644
index 0000000000..c1602f3900
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfDlcFilteredLoadTestView.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.flowui.component.genericfilter.GenericFilter;
+import io.jmix.flowui.component.propertyfilter.PropertyFilter;
+import io.jmix.flowui.view.*;
+
+/**
+ * Has a {@code DataLoadCoordinator}. Two configurations on {@code number}; the first ("match",
+ * value FLT_MATCH) is activated via the builder's {@code makeCurrent()} during {@code onInit}.
+ * Used to verify that the data actually loaded on open is filtered by the active configuration.
+ */
+@Route(value = "gf-dlc-filtered-load-test-view")
+@ViewController("GfDlcFilteredLoadTestView")
+@ViewDescriptor("gf-activation-dlc-view.xml")
+public class GfDlcFilteredLoadTestView extends StandardView {
+
+ @ViewComponent
+ public GenericFilter genericFilter;
+
+ @Subscribe
+ public void onInit(final InitEvent event) {
+ PropertyFilter match = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("match")
+ .name("Match")
+ .add(match, "FLT_MATCH")
+ .makeCurrent()
+ .buildAndRegister();
+
+ PropertyFilter other = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("other")
+ .name("Other")
+ .add(other, "FLT_OTHER")
+ .buildAndRegister();
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfDlcPlainSetCurrentTestView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfDlcPlainSetCurrentTestView.java
new file mode 100644
index 0000000000..bb080179f6
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfDlcPlainSetCurrentTestView.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.flowui.component.genericfilter.GenericFilter;
+import io.jmix.flowui.component.propertyfilter.PropertyFilter;
+import io.jmix.flowui.view.*;
+
+/**
+ * Has a {@code DataLoadCoordinator}. Two configurations on {@code number}; the first ("match",
+ * value FLT_MATCH) is activated via the base-API {@code setCurrentConfiguration(...)} during
+ * {@code onInit} (NOT the builder's {@code makeCurrent()}). Used to verify the latent base-API
+ * issue is fixed: switching to another configuration must apply only that configuration's condition.
+ */
+@Route(value = "gf-dlc-plain-setcurrent-test-view")
+@ViewController("GfDlcPlainSetCurrentTestView")
+@ViewDescriptor("gf-activation-dlc-view.xml")
+public class GfDlcPlainSetCurrentTestView extends StandardView {
+
+ @ViewComponent
+ public GenericFilter genericFilter;
+
+ @Subscribe
+ public void onInit(final InitEvent event) {
+ PropertyFilter match = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("match")
+ .name("Match")
+ .add(match, "FLT_MATCH")
+ .buildAndRegister();
+
+ PropertyFilter other = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("other")
+ .name("Other")
+ .add(other, "FLT_OTHER")
+ .buildAndRegister();
+
+ // Base-API activation in onInit (no builder makeCurrent).
+ genericFilter.setCurrentConfiguration(genericFilter.getConfiguration("match"));
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/generic-filter-initial-condition-test-view.xml b/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/generic-filter-initial-condition-test-view.xml
new file mode 100644
index 0000000000..05b212b193
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/generic-filter-initial-condition-test-view.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-activation-dlc-view.xml b/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-activation-dlc-view.xml
new file mode 100644
index 0000000000..efd7ee55de
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-activation-dlc-view.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-activation-nodlc-view.xml b/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-activation-nodlc-view.xml
new file mode 100644
index 0000000000..05b212b193
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-activation-nodlc-view.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From c1a90b32ae6c140992dbe3c624104f61629238ff Mon Sep 17 00:00:00 2001
From: aleksandrovpv
Date: Sun, 28 Jun 2026 19:08:29 +0400
Subject: [PATCH 3/3] GenericFilter: adopt externally set data loader condition
as baseline
Adopt the loader condition as the new baseline when it was replaced
externally (e.g. DataLoader.setCondition() after a configuration was
activated in onInit), instead of only capturing it before the first
filter contribution. The filter's own output is never adopted (tracked
by identity), so configuration switching is still not polluted. This
preserves backward compatibility for code that sets the base condition
after activating a configuration, and for a standalone GroupFilter,
while keeping the onInit-activation fix. Applied symmetrically to
GenericFilter and GroupFilter.
Add since/forRemoval to the deprecated updateDataLoaderInitialCondition
methods. Tests cover: base condition set after activation, base
condition revised after activation, standalone GroupFilter, and an
explicitly set initial condition.
jmix-framework/jmix#5400
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../component/genericfilter/FilterUtils.java | 2 +-
.../genericfilter/GenericFilter.java | 24 ++--
.../component/logicalfilter/GroupFilter.java | 13 ++-
.../logicalfilter/GroupFilterUtils.java | 2 +-
...terBaseConditionAfterActivationTest.groovy | 104 ++++++++++++++++++
.../GenericFilterBuilderApiTest.groovy | 2 -
.../GroupFilterBaseConditionTest.groovy | 76 +++++++++++++
.../GfBaseConditionAfterActivationView.java | 64 +++++++++++
.../view/GfBaseConditionReviseView.java | 66 +++++++++++
.../view/GfConfigsNoActivationView.java | 48 ++++++++
.../view/GfGroupFilterBaseConditionView.java | 39 +++++++
.../gf-group-filter-base-condition-view.xml | 34 ++++++
12 files changed, 451 insertions(+), 23 deletions(-)
create mode 100644 jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBaseConditionAfterActivationTest.groovy
create mode 100644 jmix-flowui/flowui/src/test/groovy/component/genericfilter/GroupFilterBaseConditionTest.groovy
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfBaseConditionAfterActivationView.java
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfBaseConditionReviseView.java
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfConfigsNoActivationView.java
create mode 100644 jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfGroupFilterBaseConditionView.java
create mode 100644 jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-group-filter-base-condition-view.xml
diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/genericfilter/FilterUtils.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/genericfilter/FilterUtils.java
index 5aaaa41088..4d9d36b373 100644
--- a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/genericfilter/FilterUtils.java
+++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/genericfilter/FilterUtils.java
@@ -52,7 +52,7 @@ public static void setCurrentConfiguration(GenericFilter filter, Configuration c
* @deprecated no longer used internally; {@link GenericFilter} now captures the initial data loader
* condition lazily. Retained for backward compatibility.
*/
- @Deprecated
+ @Deprecated(since = "3.0", forRemoval = true)
@Internal
public static void updateDataLoaderInitialCondition(GenericFilter genericFilter, @Nullable Condition condition) {
genericFilter.updateDataLoaderInitialCondition(condition);
diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/genericfilter/GenericFilter.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/genericfilter/GenericFilter.java
index c8373013b5..6e2ed5c59b 100644
--- a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/genericfilter/GenericFilter.java
+++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/genericfilter/GenericFilter.java
@@ -115,13 +115,9 @@ public class GenericFilter extends Composite
protected String applyShortcut;
protected int propertyHierarchyDepth;
protected DataLoader dataLoader;
- /**
- * The data loader's own condition (without any filter contribution), captured lazily and once
- * before the filter first updates the loader condition, then combined (AND) with the current
- * configuration on every change.
- */
protected Condition initialDataLoaderCondition;
protected boolean initialDataLoaderConditionInitialized;
+ protected Condition lastConditionSetByFilter;
protected Predicate propertyFiltersPredicate;
protected VerticalLayout contentWrapper;
@@ -368,11 +364,6 @@ public DataLoader getDataLoader() {
/**
* Sets a {@link DataLoader} related to the filter.
- *
- * The data loader's own condition is preserved and combined (AND) with the current
- * configuration's condition. It is captured once, before the filter first applies a
- * configuration; therefore any base loader condition must be set (in XML or in {@code onInit})
- * before a configuration is activated.
*
* @param dataLoader a {@link DataLoader} to set
*/
@@ -392,7 +383,7 @@ public void setDataLoader(DataLoader dataLoader) {
* in {@link #updateDataLoaderCondition()} before the first filter contribution. Retained for
* backward compatibility.
*/
- @Deprecated
+ @Deprecated(since = "3.0", forRemoval = true)
protected void updateDataLoaderInitialCondition(@Nullable Condition condition) {
this.initialDataLoaderCondition = copy(condition);
this.initialDataLoaderConditionInitialized = true;
@@ -847,10 +838,12 @@ protected String getConfigurationName(Configuration configuration) {
protected void updateDataLoaderCondition() {
if (dataLoader != null) {
- if (!initialDataLoaderConditionInitialized) {
- // Capture the data loader's own condition once, before the first filter contribution,
- // so it is never polluted by a configuration activated during onInit.
- initialDataLoaderCondition = copy(dataLoader.getCondition());
+ Condition currentCondition = dataLoader.getCondition();
+ // Re-capture the loader's own condition only when it was replaced externally (a different
+ // object than the filter's last output); the filter never adopts its own output.
+ if (!initialDataLoaderConditionInitialized
+ || (lastConditionSetByFilter != null && currentCondition != lastConditionSetByFilter)) {
+ initialDataLoaderCondition = copy(currentCondition);
initialDataLoaderConditionInitialized = true;
}
LogicalFilterComponent> logicalFilterComponent = getCurrentConfiguration().getRootLogicalFilterComponent();
@@ -869,6 +862,7 @@ protected void updateDataLoaderCondition() {
}
dataLoader.setCondition(resultCondition);
+ lastConditionSetByFilter = resultCondition;
}
}
diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilter.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilter.java
index c425a22e93..0d38cf5a1c 100644
--- a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilter.java
+++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilter.java
@@ -66,6 +66,7 @@ public class GroupFilter extends Composite
protected DataLoader dataLoader;
protected Condition initialDataLoaderCondition;
protected boolean initialDataLoaderConditionInitialized;
+ protected Condition lastConditionSetByFilter;
protected boolean autoApply;
@Internal
@@ -200,7 +201,7 @@ public void setDataLoader(DataLoader dataLoader) {
* in {@link #updateDataLoaderCondition()} before the first filter contribution. Retained for
* backward compatibility.
*/
- @Deprecated
+ @Deprecated(since = "3.0", forRemoval = true)
protected void updateDataLoaderInitialCondition(@Nullable Condition condition) {
this.initialDataLoaderCondition = copy(condition);
this.initialDataLoaderConditionInitialized = true;
@@ -211,9 +212,12 @@ protected void updateDataLoaderCondition() {
return;
}
- if (!initialDataLoaderConditionInitialized) {
- // Capture the data loader's own condition once, before the first filter contribution.
- initialDataLoaderCondition = copy(dataLoader.getCondition());
+ Condition currentCondition = dataLoader.getCondition();
+ // Re-capture the loader's own condition only when it was replaced externally (a different
+ // object than the filter's last output); the filter never adopts its own output.
+ if (!initialDataLoaderConditionInitialized
+ || (lastConditionSetByFilter != null && currentCondition != lastConditionSetByFilter)) {
+ initialDataLoaderCondition = copy(currentCondition);
initialDataLoaderConditionInitialized = true;
}
@@ -230,6 +234,7 @@ protected void updateDataLoaderCondition() {
}
dataLoader.setCondition(resultCondition);
+ lastConditionSetByFilter = resultCondition;
}
@Override
diff --git a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilterUtils.java b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilterUtils.java
index 36d3002b3a..7981527d3d 100644
--- a/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilterUtils.java
+++ b/jmix-flowui/flowui/src/main/java/io/jmix/flowui/component/logicalfilter/GroupFilterUtils.java
@@ -27,7 +27,7 @@ public class GroupFilterUtils {
* @deprecated no longer used internally; {@link GroupFilter} now captures the initial data loader
* condition lazily. Retained for backward compatibility.
*/
- @Deprecated
+ @Deprecated(since = "3.0", forRemoval = true)
public static void updateDataLoaderInitialCondition(GroupFilter groupFilter, @Nullable Condition condition) {
groupFilter.updateDataLoaderInitialCondition(condition);
}
diff --git a/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBaseConditionAfterActivationTest.groovy b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBaseConditionAfterActivationTest.groovy
new file mode 100644
index 0000000000..9dcc06f272
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBaseConditionAfterActivationTest.groovy
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter
+
+import component.genericfilter.view.GfBaseConditionAfterActivationView
+import component.genericfilter.view.GfBaseConditionReviseView
+import component.genericfilter.view.GfConfigsNoActivationView
+import io.jmix.core.querycondition.Condition
+import io.jmix.core.querycondition.LogicalCondition
+import io.jmix.core.querycondition.PropertyCondition
+import io.jmix.flowui.component.genericfilter.FilterUtils
+import io.jmix.flowui.component.genericfilter.GenericFilter
+import org.springframework.boot.test.context.SpringBootTest
+import test_support.spec.FlowuiTestSpecification
+
+/**
+ * A base condition set on the data loader after a configuration is activated in {@code onInit}
+ * must still be applied after switching configurations.
+ */
+@SpringBootTest
+class GenericFilterBaseConditionAfterActivationTest extends FlowuiTestSpecification {
+
+ void setup() {
+ registerViewBasePackages("component.genericfilter.view")
+ }
+
+ def "base condition set after activation in onInit survives a configuration switch"() {
+ when: "the view opens: c1 activated in onInit, then a base condition on 'amount' set on the loader"
+ GenericFilter filter = navigateToView(GfBaseConditionAfterActivationView).genericFilter
+
+ and: "switching to c2"
+ filter.setCurrentConfiguration(filter.getConfiguration("c2"))
+
+ then: "the base condition (on 'amount') is still applied alongside c2"
+ hasPropertyConditionOn(filter.dataLoader.condition, "amount")
+ }
+
+ def "base condition revised after activation in onInit is the one preserved on switch"() {
+ when: "the view opens: base on 'amount', activate c1, then base revised to 'total' — all in onInit"
+ GenericFilter filter = navigateToView(GfBaseConditionReviseView).genericFilter
+
+ and: "switching to c2"
+ filter.setCurrentConfiguration(filter.getConfiguration("c2"))
+
+ then: "the revised base condition (on 'total') is applied, not the pre-activation one"
+ hasPropertyConditionOn(filter.dataLoader.condition, "total")
+ }
+
+ def "explicitly set initial condition is preserved when a configuration is later activated"() {
+ given: "the view opens with a configuration built but not activated (filter has not contributed yet)"
+ GenericFilter filter = navigateToView(GfConfigsNoActivationView).genericFilter
+
+ and: "the loader already holds some condition, and a DIFFERENT initial condition is set explicitly"
+ filter.dataLoader.setCondition(PropertyCondition.equal("number", "X"))
+ FilterUtils.updateDataLoaderInitialCondition(filter, PropertyCondition.greater("amount", 0))
+
+ when: "a configuration is activated (the first filter contribution)"
+ filter.setCurrentConfiguration(filter.getConfiguration("c1"))
+
+ then: "the explicitly set initial condition (on 'amount') is used, not the loader's prior condition"
+ hasPropertyConditionOn(filter.dataLoader.condition, "amount")
+ }
+
+ def "a logical base condition is preserved and combined with the active configuration"() {
+ given: "the view opens with a configuration built but not activated"
+ GenericFilter filter = navigateToView(GfConfigsNoActivationView).genericFilter
+
+ and: "the loader has a logical base condition with two properties"
+ filter.dataLoader.setCondition(LogicalCondition.and(
+ PropertyCondition.greater("amount", 0),
+ PropertyCondition.greater("total", 0)))
+
+ when: "a configuration is activated"
+ filter.setCurrentConfiguration(filter.getConfiguration("c1"))
+
+ then: "both base conditions are still applied alongside the configuration"
+ hasPropertyConditionOn(filter.dataLoader.condition, "amount")
+ hasPropertyConditionOn(filter.dataLoader.condition, "total")
+ }
+
+ protected static boolean hasPropertyConditionOn(Condition condition, String property) {
+ if (condition instanceof PropertyCondition) {
+ return property == condition.property
+ }
+ if (condition instanceof LogicalCondition) {
+ return condition.conditions.any { hasPropertyConditionOn(it, property) }
+ }
+ return false
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBuilderApiTest.groovy b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBuilderApiTest.groovy
index 71716ce950..2d0e13333f 100644
--- a/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBuilderApiTest.groovy
+++ b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GenericFilterBuilderApiTest.groovy
@@ -854,8 +854,6 @@ class GenericFilterBuilderApiTest extends FlowuiTestSpecification {
noExceptionThrown()
}
- // Builder options and RunTimeConfigurationBuilder edge branches
-
def "PropertyFilterBuilder.label() sets the label on the built PropertyFilter"() {
given:
GenericFilter filter = filterWithLoader()
diff --git a/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GroupFilterBaseConditionTest.groovy b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GroupFilterBaseConditionTest.groovy
new file mode 100644
index 0000000000..0212354ff1
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/groovy/component/genericfilter/GroupFilterBaseConditionTest.groovy
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter
+
+import component.genericfilter.view.GfGroupFilterBaseConditionView
+import io.jmix.core.querycondition.Condition
+import io.jmix.core.querycondition.LogicalCondition
+import io.jmix.core.querycondition.PropertyCondition
+import io.jmix.flowui.component.logicalfilter.GroupFilter
+import io.jmix.flowui.component.logicalfilter.LogicalFilterComponent
+import org.springframework.boot.test.context.SpringBootTest
+import test_support.spec.FlowuiTestSpecification
+
+/**
+ * A base condition set on the data loader of a standalone {@code GroupFilter} in {@code onInit}
+ * must still be applied after the loader condition is rebuilt (e.g. on an operation change).
+ */
+@SpringBootTest
+class GroupFilterBaseConditionTest extends FlowuiTestSpecification {
+
+ void setup() {
+ registerViewBasePackages("component.genericfilter.view")
+ }
+
+ def "base condition set in onInit on a standalone GroupFilter survives a structural rebuild"() {
+ when: "the view opens: standalone GroupFilter with a base condition on 'amount' set in onInit"
+ GroupFilter groupFilter = navigateToView(GfGroupFilterBaseConditionView).groupFilter
+
+ and: "a structural change (operation switch) forces the loader condition to be rebuilt"
+ groupFilter.setOperation(LogicalFilterComponent.Operation.OR)
+
+ then: "the base condition (on 'amount') is still applied"
+ hasPropertyConditionOn(groupFilter.dataLoader.condition, "amount")
+ }
+
+ def "a logical base condition on a standalone GroupFilter is preserved on a structural rebuild"() {
+ given: "the view opens with a standalone GroupFilter"
+ GroupFilter groupFilter = navigateToView(GfGroupFilterBaseConditionView).groupFilter
+
+ and: "the loader has a logical base condition with two properties"
+ groupFilter.dataLoader.setCondition(LogicalCondition.and(
+ PropertyCondition.greater("amount", 0),
+ PropertyCondition.greater("total", 0)))
+
+ when: "a structural change forces the loader condition to be rebuilt"
+ groupFilter.setOperation(LogicalFilterComponent.Operation.OR)
+
+ then: "both base conditions are still applied"
+ hasPropertyConditionOn(groupFilter.dataLoader.condition, "amount")
+ hasPropertyConditionOn(groupFilter.dataLoader.condition, "total")
+ }
+
+ protected static boolean hasPropertyConditionOn(Condition condition, String property) {
+ if (condition instanceof PropertyCondition) {
+ return property == condition.property
+ }
+ if (condition instanceof LogicalCondition) {
+ return condition.conditions.any { hasPropertyConditionOn(it, property) }
+ }
+ return false
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfBaseConditionAfterActivationView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfBaseConditionAfterActivationView.java
new file mode 100644
index 0000000000..c20b3a03d8
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfBaseConditionAfterActivationView.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.core.querycondition.PropertyCondition;
+import io.jmix.flowui.component.genericfilter.GenericFilter;
+import io.jmix.flowui.component.genericfilter.configuration.RunTimeConfiguration;
+import io.jmix.flowui.component.propertyfilter.PropertyFilter;
+import io.jmix.flowui.view.*;
+
+/**
+ * Sets a base condition on the data loader after activating a configuration in {@code onInit}.
+ */
+@Route(value = "gf-base-after-activation-view")
+@ViewController("GfBaseConditionAfterActivationView")
+@ViewDescriptor("gf-activation-nodlc-view.xml")
+public class GfBaseConditionAfterActivationView extends StandardView {
+
+ @ViewComponent
+ public GenericFilter genericFilter;
+
+ @Subscribe
+ public void onInit(final InitEvent event) {
+ PropertyFilter number1 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ RunTimeConfiguration c1 = genericFilter.runtimeConfigurationBuilder()
+ .id("c1")
+ .name("C1")
+ .add(number1, "n1")
+ .buildAndRegister();
+
+ PropertyFilter number2 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c2")
+ .name("C2")
+ .add(number2, "n2")
+ .buildAndRegister();
+
+ genericFilter.setCurrentConfiguration(c1);
+ genericFilter.getDataLoader().setCondition(PropertyCondition.greater("amount", 0));
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfBaseConditionReviseView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfBaseConditionReviseView.java
new file mode 100644
index 0000000000..7b5e2139af
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfBaseConditionReviseView.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.core.querycondition.PropertyCondition;
+import io.jmix.flowui.component.genericfilter.GenericFilter;
+import io.jmix.flowui.component.genericfilter.configuration.RunTimeConfiguration;
+import io.jmix.flowui.component.propertyfilter.PropertyFilter;
+import io.jmix.flowui.view.*;
+
+/**
+ * Sets a base condition, activates a configuration, then revises the base condition, all in
+ * {@code onInit}.
+ */
+@Route(value = "gf-base-revise-view")
+@ViewController("GfBaseConditionReviseView")
+@ViewDescriptor("gf-activation-nodlc-view.xml")
+public class GfBaseConditionReviseView extends StandardView {
+
+ @ViewComponent
+ public GenericFilter genericFilter;
+
+ @Subscribe
+ public void onInit(final InitEvent event) {
+ PropertyFilter number1 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ RunTimeConfiguration c1 = genericFilter.runtimeConfigurationBuilder()
+ .id("c1")
+ .name("C1")
+ .add(number1, "n1")
+ .buildAndRegister();
+
+ PropertyFilter number2 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c2")
+ .name("C2")
+ .add(number2, "n2")
+ .buildAndRegister();
+
+ genericFilter.getDataLoader().setCondition(PropertyCondition.greater("amount", 0));
+ genericFilter.setCurrentConfiguration(c1);
+ genericFilter.getDataLoader().setCondition(PropertyCondition.greater("total", 0));
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfConfigsNoActivationView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfConfigsNoActivationView.java
new file mode 100644
index 0000000000..b4780d527a
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfConfigsNoActivationView.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.flowui.component.genericfilter.GenericFilter;
+import io.jmix.flowui.component.propertyfilter.PropertyFilter;
+import io.jmix.flowui.view.*;
+
+/**
+ * Builds a configuration but does not activate it in {@code onInit}.
+ */
+@Route(value = "gf-configs-no-activation-view")
+@ViewController("GfConfigsNoActivationView")
+@ViewDescriptor("gf-activation-nodlc-view.xml")
+public class GfConfigsNoActivationView extends StandardView {
+
+ @ViewComponent
+ public GenericFilter genericFilter;
+
+ @Subscribe
+ public void onInit(final InitEvent event) {
+ PropertyFilter number1 = genericFilter.filterComponentBuilder()
+ .propertyFilter()
+ .property("number")
+ .operation(PropertyFilter.Operation.EQUAL)
+ .build();
+ genericFilter.runtimeConfigurationBuilder()
+ .id("c1")
+ .name("C1")
+ .add(number1, "n1")
+ .buildAndRegister();
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfGroupFilterBaseConditionView.java b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfGroupFilterBaseConditionView.java
new file mode 100644
index 0000000000..515f9b444b
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/java/component/genericfilter/view/GfGroupFilterBaseConditionView.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2026 Haulmont.
+ *
+ * Licensed 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 component.genericfilter.view;
+
+import com.vaadin.flow.router.Route;
+import io.jmix.core.querycondition.PropertyCondition;
+import io.jmix.flowui.component.logicalfilter.GroupFilter;
+import io.jmix.flowui.view.*;
+
+/**
+ * A standalone {@code GroupFilter} with a base condition set on its data loader in {@code onInit}.
+ */
+@Route(value = "gf-group-filter-base-condition-view")
+@ViewController("GfGroupFilterBaseConditionView")
+@ViewDescriptor("gf-group-filter-base-condition-view.xml")
+public class GfGroupFilterBaseConditionView extends StandardView {
+
+ @ViewComponent
+ public GroupFilter groupFilter;
+
+ @Subscribe
+ public void onInit(final InitEvent event) {
+ groupFilter.getDataLoader().setCondition(PropertyCondition.greater("amount", 0));
+ }
+}
diff --git a/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-group-filter-base-condition-view.xml b/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-group-filter-base-condition-view.xml
new file mode 100644
index 0000000000..b0af5b9182
--- /dev/null
+++ b/jmix-flowui/flowui/src/test/resources/component/genericfilter/view/gf-group-filter-base-condition-view.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+