Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,16 @@ public class UiComponentProperties {
*/
boolean filterConfigurationUniqueNamesEnabled;

/**
* Whether a standalone filter component ({@link io.jmix.flowui.component.propertyfilter.PropertyFilter},
* {@link io.jmix.flowui.component.jpqlfilter.JpqlFilter}, a {@code DataGrid} header filter) participates
* in loading as a condition contributor of its data loader instead of appending its condition into the
* loader's condition tree. With contributors the loader's condition stays with the application, the
* contribution cannot be lost when another party replaces or rebuilds the loader condition, and it is
* not visible in {@code DataLoader#getCondition()} - only in {@code DataLoader#getEffectiveCondition()}.
*/
boolean standaloneFilterContributesCondition;

/**
* Whether error message should be shown below the field or not.
*/
Expand Down Expand Up @@ -195,6 +205,7 @@ public UiComponentProperties(
@DefaultValue("false") boolean filterShowConfigurationIdField,
@DefaultValue("true") boolean filterShowNonJpaProperties,
@DefaultValue("true") boolean filterConfigurationUniqueNamesEnabled,
@DefaultValue("false") boolean standaloneFilterContributesCondition,
@DefaultValue("true") boolean showErrorMessageBelowField,
@DefaultValue("true") boolean immediateRequiredValidationEnabled,
@DefaultValue("true") boolean defaultTrimEnabled,
Expand Down Expand Up @@ -231,6 +242,7 @@ public UiComponentProperties(
this.filterShowConfigurationIdField = filterShowConfigurationIdField;
this.filterShowNonJpaProperties = filterShowNonJpaProperties;
this.filterConfigurationUniqueNamesEnabled = filterConfigurationUniqueNamesEnabled;
this.standaloneFilterContributesCondition = standaloneFilterContributesCondition;

this.showErrorMessageBelowField = showErrorMessageBelowField;
this.immediateRequiredValidationEnabled = immediateRequiredValidationEnabled;
Expand Down Expand Up @@ -383,6 +395,13 @@ public boolean isFilterConfigurationUniqueNamesEnabled() {
return filterConfigurationUniqueNamesEnabled;
}

/**
* @see #standaloneFilterContributesCondition
*/
public boolean isStandaloneFilterContributesCondition() {
return standaloneFilterContributesCondition;
}

/**
* @see #showErrorMessageBelowField
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.vaadin.flow.dom.PropertyChangeEvent;
import com.vaadin.flow.shared.Registration;
import io.jmix.core.annotation.Internal;
import io.jmix.core.common.event.Subscription;
import io.jmix.core.querycondition.Condition;
import io.jmix.core.querycondition.LogicalCondition;
import io.jmix.flowui.UiComponentProperties;
Expand Down Expand Up @@ -61,6 +62,9 @@ public abstract class SingleFilterComponentBase<V> extends CustomField<V>
protected DataLoader dataLoader;
protected boolean autoApply;
protected Condition queryCondition;
protected boolean contributesCondition;
@Nullable
protected Subscription conditionContributorSubscription;

@Internal
protected boolean conditionModificationDelegated = false;
Expand Down Expand Up @@ -93,7 +97,9 @@ protected void autowireDependencies() {
}

protected void initComponent() {
this.autoApply = applicationContext.getBean(UiComponentProperties.class).isFilterAutoApply();
UiComponentProperties componentProperties = applicationContext.getBean(UiComponentProperties.class);
this.autoApply = componentProperties.isFilterAutoApply();
this.contributesCondition = componentProperties.isStandaloneFilterContributesCondition();

root = createRootComponent();
initRootComponent(root);
Expand Down Expand Up @@ -156,7 +162,14 @@ public void setDataLoader(DataLoader dataLoader) {
this.dataLoader = dataLoader;

if (!isConditionModificationDelegated()) {
updateDataLoaderCondition();
if (contributesCondition) {
// The contributor mode: the loader polls the filter for its current condition on
// every load, the loader's condition slot stays with the application, and the
// contribution cannot be lost when another party replaces or rebuilds the slot.
conditionContributorSubscription = dataLoader.addConditionContributor(this::getQueryCondition);
} else {
updateDataLoaderCondition();
}
}
}

Expand Down Expand Up @@ -195,6 +208,13 @@ public boolean isConditionModificationDelegated() {
@Override
public void setConditionModificationDelegated(boolean conditionModificationDelegated) {
this.conditionModificationDelegated = conditionModificationDelegated;

if (conditionModificationDelegated && conditionContributorSubscription != null) {
// The owner takes over condition management: a contribution of its own would now
// duplicate the owner's composition, which includes this filter's condition.
conditionContributorSubscription.remove();
conditionContributorSubscription = null;
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 io.jmix.flowui.model;

import io.jmix.core.querycondition.Condition;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

/**
* Supplies a condition to a {@link DataLoader} the contributor is registered on. The loader polls
* every registered contributor when it builds a load context, so the query condition is composed
* of the loader's own condition and the current contribution of each contributor at the moment of
* loading. The loader takes a copy of the returned condition: a contribution never becomes a
* shared mutable node of the loader's condition tree, and editing a previously returned condition
* takes effect on the next load.
*
* <p>Contributors let several independent parties filter one loader without competing for the
* single {@link DataLoader#setCondition(Condition)} slot: the slot stays with the application,
* each contributor owns its contribution.
*
* @see DataLoader#addConditionContributor(ConditionContributor)
*/
@NullMarked
@FunctionalInterface
public interface ConditionContributor {

/**
* Returns the current contribution of this contributor, or {@code null} if it currently
* contributes nothing.
*/
@Nullable
Condition getCondition();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.jmix.flowui.model;

import io.jmix.core.common.event.Subscription;
import io.jmix.core.querycondition.Condition;

import io.jmix.flowui.monitoring.DataLoaderMonitoringInfo;
Expand Down Expand Up @@ -79,6 +80,33 @@ public interface DataLoader {
*/
void setCondition(@Nullable Condition condition);

/**
* Registers a contributor whose current condition is combined with the condition of this
* loader on every load. The resulting query condition is the conjunction of
* {@link #getCondition()} and the non-null contributions; the loader copies each contribution,
* so a contributor keeps sole ownership of its condition instance.
*
* @param conditionContributor the contributor to register
* @return a subscription that unregisters the contributor
* @throws UnsupportedOperationException if this implementation does not support condition
* contributors
*/
default Subscription addConditionContributor(ConditionContributor conditionContributor) {
throw new UnsupportedOperationException(
getClass().getName() + " does not support condition contributors");
}

/**
* Returns the condition the next load would use: the conjunction of {@link #getCondition()}
* and the current contributions of the registered condition contributors. With no registered
* contributors it is the same as {@link #getCondition()}. The returned condition is composed
* for reading; modifying it has no effect on this loader.
*/
@Nullable
default Condition getEffectiveCondition() {
return getCondition();
}

/**
* Returns the map of query parameters.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public class CollectionLoaderImpl<E> implements CollectionLoader<E> {
protected CollectionContainer<E> container;
protected String query;
protected Condition condition;
protected List<ConditionContributor> conditionContributors = new ArrayList<>();
protected Map<String, Object> parameters = new HashMap<>();
protected int firstResult = 0;
protected int maxResults = Integer.MAX_VALUE;
Expand Down Expand Up @@ -165,12 +166,13 @@ public LoadContext<E> createLoadContext() {

LoadContext.Query query = loadContext.setQueryString(queryString);

query.setCondition(condition);
Condition effectiveCondition = getEffectiveCondition();
query.setCondition(effectiveCondition);
query.setSort(sort);
query.setParameters(parameters);

query.setCacheable(cacheable);
query.setDistinct(canLeadToDuplicateResultsRecursive(condition));
query.setDistinct(canLeadToDuplicateResultsRecursive(effectiveCondition));

if (firstResult > 0)
query.setFirstResult(firstResult);
Expand Down Expand Up @@ -287,6 +289,19 @@ public void setCondition(@Nullable Condition condition) {
this.condition = condition;
}

@Override
public Subscription addConditionContributor(ConditionContributor conditionContributor) {
Preconditions.checkNotNullArgument(conditionContributor);
conditionContributors.add(conditionContributor);
return () -> conditionContributors.remove(conditionContributor);
}

@Nullable
@Override
public Condition getEffectiveCondition() {
return DataLoadersHelper.composeEffectiveCondition(condition, conditionContributors);
}

@Override
public Map<String, Object> getParameters() {
return Collections.unmodifiableMap(parameters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
package io.jmix.flowui.model.impl;

import com.google.common.base.Strings;
import io.jmix.core.querycondition.Condition;
import io.jmix.core.querycondition.LogicalCondition;
import io.jmix.flowui.model.ConditionContributor;
import io.jmix.flowui.model.DataLoader;
import io.jmix.flowui.model.HasLoader;
import io.jmix.flowui.model.InstanceContainer;
Expand All @@ -35,6 +38,40 @@ public class DataLoadersHelper {

public static final Pattern PARAM_PATTERN = Pattern.compile(":([\\w$]+)");

/**
* Composes the effective condition of a loader: the conjunction of the loader's own condition
* and the current contributions of the registered contributors. Both the loader's condition
* and the contributions enter the composed tree as copies, so it shares no nodes with anyone.
* With no non-null contributions the loader's own condition is returned as is - the same
* instance, so the behavior of a loader without contributors is untouched.
*
* @param condition the loader's own condition
* @param contributors registered contributors, polled in registration order
* @return the composed condition, or {@code null} if there is nothing to compose
*/
@Nullable
public static Condition composeEffectiveCondition(@Nullable Condition condition,
List<ConditionContributor> contributors) {
List<Condition> contributions = new ArrayList<>(contributors.size());
for (ConditionContributor contributor : contributors) {
Condition contribution = contributor.getCondition();
if (contribution != null) {
contributions.add(contribution.copy());
}
}

if (contributions.isEmpty()) {
return condition;
}

LogicalCondition effective = LogicalCondition.and();
if (condition != null) {
effective.add(condition.copy());
}
contributions.forEach(effective::add);
return effective;
}

/**
* Returns the loader of master entity instance.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ public class InstanceLoaderImpl<E> implements InstanceLoader<E> {
protected InstanceContainer<E> container;
protected String query;
protected Condition condition;
protected List<ConditionContributor> conditionContributors = new ArrayList<>();
protected Map<String, Object> parameters = new HashMap<>();
protected Object entityId;
protected FetchPlan fetchPlan;
Expand Down Expand Up @@ -156,7 +157,8 @@ public LoadContext<E> createLoadContext() {
} else {
String queryString = QueryUtils.applyQueryStringProcessors(queryStringProcessors, this.query, entityClass);
LoadContext.Query query = loadContext.setQueryString(queryString);
query.setCondition(condition);
Condition effectiveCondition = getEffectiveCondition();
query.setCondition(effectiveCondition);
query.setParameters(parameters);
}

Expand Down Expand Up @@ -224,6 +226,19 @@ public void setCondition(@Nullable Condition condition) {
this.condition = condition;
}

@Override
public Subscription addConditionContributor(ConditionContributor conditionContributor) {
Preconditions.checkNotNullArgument(conditionContributor);
conditionContributors.add(conditionContributor);
return () -> conditionContributors.remove(conditionContributor);
}

@Nullable
@Override
public Condition getEffectiveCondition() {
return DataLoadersHelper.composeEffectiveCondition(condition, conditionContributors);
}

@Override
public Map<String, Object> getParameters() {
return Collections.unmodifiableMap(parameters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public class KeyValueCollectionLoaderImpl implements KeyValueCollectionLoader {
protected KeyValueCollectionContainer container;
protected String query;
protected Condition condition;
protected List<ConditionContributor> conditionContributors = new ArrayList<>();
protected Map<String, Object> parameters = new HashMap<>();
protected int firstResult = 0;
protected int maxResults = Integer.MAX_VALUE;
Expand Down Expand Up @@ -136,7 +137,8 @@ public ValueLoadContext createLoadContext() {

ValueLoadContext.Query query = loadContext.setQueryString(this.query);

query.setCondition(condition);
Condition effectiveCondition = getEffectiveCondition();
query.setCondition(effectiveCondition);
query.setSort(sort);
query.setParameters(parameters);

Expand Down Expand Up @@ -198,6 +200,19 @@ public void setCondition(@Nullable Condition condition) {
this.condition = condition;
}

@Override
public Subscription addConditionContributor(ConditionContributor conditionContributor) {
Preconditions.checkNotNullArgument(conditionContributor);
conditionContributors.add(conditionContributor);
return () -> conditionContributors.remove(conditionContributor);
}

@Nullable
@Override
public Condition getEffectiveCondition() {
return DataLoadersHelper.composeEffectiveCondition(condition, conditionContributors);
}

@Override
public Map<String, Object> getParameters() {
return Collections.unmodifiableMap(parameters);
Expand Down
Loading