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 f0217ceaba..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 @@ -48,6 +48,11 @@ public static void setCurrentConfiguration(GenericFilter filter, Configuration c filter.setCurrentConfigurationInternal(currentConfiguration, fromClient); } + /** + * @deprecated no longer used internally; {@link GenericFilter} now captures the initial data loader + * condition lazily. Retained for backward compatibility. + */ + @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 3d8ab1b2be..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 @@ -116,6 +116,8 @@ public class GenericFilter extends Composite protected int propertyHierarchyDepth; protected DataLoader dataLoader; protected Condition initialDataLoaderCondition; + protected boolean initialDataLoaderConditionInitialized; + protected Condition lastConditionSetByFilter; protected Predicate propertyFiltersPredicate; protected VerticalLayout contentWrapper; @@ -370,15 +372,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(since = "3.0", forRemoval = true) protected void updateDataLoaderInitialCondition(@Nullable Condition condition) { this.initialDataLoaderCondition = copy(condition); + this.initialDataLoaderConditionInitialized = true; } /** @@ -830,6 +838,14 @@ protected String getConfigurationName(Configuration configuration) { protected void updateDataLoaderCondition() { if (dataLoader != null) { + 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(); LogicalCondition filterCondition = logicalFilterComponent.getQueryCondition(); @@ -846,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 8372409ce1..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 @@ -65,6 +65,8 @@ public class GroupFilter extends Composite protected DataLoader dataLoader; protected Condition initialDataLoaderCondition; + protected boolean initialDataLoaderConditionInitialized; + protected Condition lastConditionSetByFilter; protected boolean autoApply; @Internal @@ -186,7 +188,6 @@ public void setDataLoader(DataLoader dataLoader) { checkNotNull(dataLoader); this.dataLoader = dataLoader; - this.initialDataLoaderCondition = dataLoader.getCondition(); if (!isConditionModificationDelegated()) { updateDataLoaderCondition(); @@ -195,8 +196,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(since = "3.0", forRemoval = true) protected void updateDataLoaderInitialCondition(@Nullable Condition condition) { this.initialDataLoaderCondition = copy(condition); + this.initialDataLoaderConditionInitialized = true; } protected void updateDataLoaderCondition() { @@ -204,6 +212,15 @@ protected void updateDataLoaderCondition() { return; } + 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; + } + LogicalCondition resultCondition; if (initialDataLoaderCondition instanceof LogicalCondition initialLogicalCondition) { resultCondition = ((LogicalCondition) copy(initialLogicalCondition)); @@ -217,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 6c7fc4867a..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 @@ -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(since = "3.0", forRemoval = true) 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()); - } - }); }); } 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 6e6d493a8e..2d0e13333f 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,86 @@ class GenericFilterBuilderApiTest extends FlowuiTestSpecification { then: noExceptionThrown() } + + 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/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/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/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/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/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/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 @@ + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + + + + + +