Добрый день!
Мне нужно создать фильтр в браузере экрана, выглядеть он должен примерно следующим образом:
Фильтр должен отрабатывать только по кнопке “Поиск”. По кнопке “Очистить” все условия фильтрация сбрасываются. При открытии браузера экрана фильтр всегда развернут. А при переходе между страницами приложения выбранные ранее условия фильтрации не сбрасываются. Использование стандартного genericFilter
как я понимаю мне не подойдет. И, судя по всему, нужно будет самому переопределить действия на кнопках “Поиск” и “Очистить”.
Подскажите, можно ли мне будет реализовать такой фильтр и как это можно сделать? Потому как стандартные компоненты фильтрации мне тут не особо подходят.
Заранее спасибо!
Добрый день!
Если конфигурация фильтра (условия) остаются неизменными, то можно набрасать такую разметку самому. Например, для простоты можно использовать PropertyFilter в качестве компонента для каждого условия. В принципе, можно использовать и обычные компоненты, но тогда нужно будет вручную создавать объекты PropertyCondition
и добавлять их в loader.
Минимальный пример, с которого можно начать:
XML
<vbox id="orderFilterBox">
<hbox id="conditionsBox">
<propertyFilter property="number" operation="STARTS_WITH" dataLoader="ordersDl" autoApply="false"/>
<propertyFilter property="startDate" operation="EQUAL" dataLoader="ordersDl" autoApply="false"/>
<propertyFilter property="active" operation="EQUAL" dataLoader="ordersDl" autoApply="false"/>
<propertyFilter property="status" operation="EQUAL" dataLoader="ordersDl" autoApply="false"/>
</hbox>
<hbox id="actionsBox">
<button id="searchBtn" text="Search" icon="SEARCH"/>
<button id="clearBtn" text="Search"/>
</hbox>
</vbox>
JAVA
@ViewComponent
private CollectionLoader<Order> ordersDl;
@Subscribe(id = "searchBtn", subject = "clickListener")
public void onSearchBtnClick(final ClickEvent<JmixButton> event) {
ordersDl.load();
}
Если такой фильтр планиурется переиспользовать в экранах, то стоит задуматься о вынесении его в композитный компонент:
Если имеются в виду значения условий, то воспользоваться фасетом settings :: Документация Jmix. Через него можно сохранять значения фильтров в экране для конктретного юзера при закртыии, а затем восстанавливать при открытии экрана.
@pinyazhin Роман, добрый день! Спасибо за ответ!
Использование PropertyFilter
мне не подойдет, т.к. для некоторых условий у меня должен использоваться компонент multiSelectComboBox
, а через PropertyFilter
его не задашь.
Я накидал следующую конфигурацию:
<vbox id="agentFilterBox">
<hbox alignItems="END" classNames="flex-wrap">
<multiSelectComboBox id="nameFilterComboBox" property="name" label="Название" width="15em"/>
<multiSelectComboBox id="statusFilterComboBox" itemsEnum="ru.ctsg.damdbf.manager.core.enums.AgentStatus" label="Статус" width="15em"/>
<multiSelectComboBox id="hostFilterComboBox" property="host" label="Хост" width="15em"/>
<dateTimePicker id="lastStartDateFilterDateTimePicker" property="lastStartDate" label="Запущен" width="15em"/>
<dateTimePicker id="transitionDateFilterDateTimePicker" property="transitionDate" label="Последнее обновление" width="15em"/>
</hbox>
<hbox id="actionsBox">
<button id="refreshFilterBtn" action="agentsDataGrid.refresh"/>
<button id="clearFilterBtn" action="agentsDataGrid.clear"/>
</hbox>
</vbox>
и есть два вопроса:
- Вы указали, что
В принципе, можно использовать и обычные компоненты, но тогда нужно будет вручную создавать объекты
PropertyCondition
и добавлять их в loader.
Можете уточнить, что тут конкретно подразумевается? Я взял вот такой пример из sampl’ов
https://demo.jmix.io/ui-samples/sample/custom-filter
В query
указал condition через И
условие:
<collection id="agentsDc"
class="ru.ctsg.damdbf.manager.core.entity.Agent">
<fetchPlan extends="_base"/>
<loader id="agentsDl">
<query>
<![CDATA[select e from Agent e]]>
<condition>
<and>
<c:jpql>
<c:where>e.name like :component_nameFilterComboBox</c:where>
</c:jpql>
<c:jpql>
<c:where>e.status in :component_statusFilterComboBox</c:where>
</c:jpql>
<c:jpql>
<c:where>e.host like :component_hostFilterComboBox</c:where>
</c:jpql>
<c:jpql>
<c:where>e.lastStartDate >= :component_lastStartDateFilterDateTimePicker</c:where>
</c:jpql>
<c:jpql>
<c:where>e.transitionDate >= :component_transitionDateFilterDateTimePicker</c:where>
</c:jpql>
</and>
</condition>
</query>
</loader>
</collection>
Я еще не проверял, но разве это не будет работать?
2. Вы указали, что для сохранения значений в моем кастомном фильтре можно использовать settings
. Но в документации указано, что он используется для dataGrid, treeDataGrid, details, genericFilter, simplePagination
. Ничего из этого мне не подходит. Тут что делать? Или мне нужно самому переопределить делегат сохранения значений в фильтре?
-
Так тоже будет работать. Программное же создание Condition-ов больше с прицелом на переиспользование в качестве отдельного компонента.
-
Это компоненты, которые поддерживаются из коробки. В остальных случаях нужно указывать делегаты на сохранение и восстановление:
@ViewComponent
private SettingsFacet settingsFacet;
@Install(to = "settingsFacet", subject = "saveSettingsDelegate")
private void settingsFacetSaveSettingsDelegate(final SettingsFacet.SettingsContext settingsContext) {
settingsContext.getViewSettings().put("nameFilterComboBox", "value", nameFilterComboBox.getValue() + "");
settingsFacet.saveSettings();
}
@Install(to = "settingsFacet", subject = "applyDataLoadingSettingsDelegate")
private void settingsFacetApplyDataLoadingSettingsDelegate(final SettingsFacet.SettingsContext settingsContext) {
settingsFacet.applyDataLoadingSettings();
settingsContext.getViewSettings()
.getString("nameFilterComboBox", "value")
.ifPresent(value -> nameFilterComboBox.setValue(value));
}
Метод settingsFacetApplyDataLoadingSettingsDelegate()
вызывается до BeforeShowEvent
, когда ещё не загружены данные. Но тут стоит обратить внимание, по умолчанию при изменении значения в компоненте, сразу проставляется в параметр loader-а и тригерится загрузка. При этом дополнительно тригерится стандартная загрузка на BeforeShowEvent
, поэтому возможна двойная загрузка данных.
Посмотрите как код будет работать, возможно нужно будет настроить фасет dataLoadCoordinator :: Документация Jmix, чтобы он грузил данные только на BeforeShowEvent, а параметры проставлять в loader вручную. Либо полностью управлять загрузкой данных вручную.
@pinyazhin спасибо! Единственное я не могу у себя в контроллере экрана через Generate Handler
сгенерировать эти события, т.к. их у меня просто нет. Что нужно и куда добавить, чтобы они появились?
Нужно добавить фасет settings
с ID:
<facets>
<settings id="settingsFacet"/>
</facets>
Когда ID будет указан, появятся хенделры в диалоге Generate Handler
.
Спасибо, все получилось!) У меня последний вопрос: при нажатии на кнопку Очистить
вся фильтрация должна сбрасываться и таблица должна возвращаться в изначальное состояние и содержать все записи. Но у меня так не работает. Я для action clear
реализовал следующую логику:
@Subscribe("agentsDataGrid.clear")
public void onAgentsDataGridClear(final ActionPerformedEvent event) {
nameFilterComboBox.clear();
statusFilterComboBox.clear();
hostFilterComboBox.clear();
transitionDateFilterDateTimePicker.clear();
lastStartDateFilterDateTimePicker.clear();
agentsDl.load();
}
но смотрю, что agentsDl
по-прежнему содержит в query результат фильтрации по имени:
select e from Agent e where e.name in :names
Только перезагрузка страницы возвращает таблицу в исходное состояние. Подскажите пожалуйста, что делаю не так?
Сейчас сложно сказать, почему параметр остаётся. Вы оставили настройку dataLoadCoorindator
или поменяли? Если не сложно, можете поделиться кодом контроллера экрана и его XML?
Если нет возможности, попробуйте подебажить. За загрузку данных отвечает CollectionLoaderImpl
, в нём же хранятся параметры и Condition-ы.
Да, конечно:
XML
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<view xmlns="http://jmix.io/schema/flowui/view"
xmlns:c="http://jmix.io/schema/flowui/jpql-condition"
title="msg://agentListView.title"
focusComponent="agentsDataGrid">
<data readOnly="true">
<collection id="agentsDc"
class="ru.ctsg.damdbf.manager.core.entity.Agent">
<fetchPlan extends="_base"/>
<loader id="agentsDl">
<query>
<![CDATA[select e from Agent e]]>
</query>
</loader>
</collection>
</data>
<facets>
<dataLoadCoordinator auto="true"/>
<urlQueryParameters>
<genericFilter component="genericFilter"/>
<pagination component="pagination" maxResultsParam="10"/>
</urlQueryParameters>
<settings id="settingsFacet"/>
</facets>
<actions>
<action id="selectAction" type="lookup_select"/>
<action id="discardAction" type="lookup_discard"/>
</actions>
<layout>
<genericFilter id="genericFilter"
dataLoader="agentsDl">
<properties include=".*"/>
</genericFilter>
<vbox id="agentFilterBox">
<hbox alignItems="END" classNames="flex-wrap">
<multiSelectComboBox id="nameFilterComboBox" property="name" dataContainer="agentsDc" label="Название"
width="15em">
<itemsQuery escapeValueForLike="true"
searchStringFormat="(?i)%${inputString}%">
<query>
<![CDATA[select e.name from Agent e where e.name
like :searchString escape '\' order by e.name asc]]>
</query>
</itemsQuery>
</multiSelectComboBox>
<multiSelectComboBox id="statusFilterComboBox" itemsEnum="ru.ctsg.damdbf.manager.core.enums.AgentStatus"
label="Статус" width="15em"/>
<multiSelectComboBox id="hostFilterComboBox" property="host" label="Хост" width="15em">
<itemsQuery escapeValueForLike="true"
searchStringFormat="(?i)%${inputString}%">
<query>
<![CDATA[select e.host from Agent e where e.host
like :searchString escape '\' order by e.host asc]]>
</query>
</itemsQuery>
</multiSelectComboBox>
<dateTimePicker id="lastStartDateFilterDateTimePicker" property="lastStartDate" label="Запущен"
width="15em"/>
<dateTimePicker id="transitionDateFilterDateTimePicker" property="transitionDate"
label="Последнее обновление" width="15em"/>
</hbox>
<hbox id="actionsBox">
<button id="refreshFilterBtn" action="agentsDataGrid.refresh"/>
<button id="clearFilterBtn" action="agentsDataGrid.clear"/>
</hbox>
</vbox>
<hbox id="buttonsPanel" classNames="buttons-panel">
<button id="startAgentBtn" action="agentsDataGrid.startAgent"/>
<button id="stopAgentBtn" action="agentsDataGrid.stopAgent"/>
<button id="removeBtn" action="agentsDataGrid.remove"/>
</hbox>
<dataGrid id="agentsDataGrid"
width="100%"
minHeight="20em"
dataContainer="agentsDc"
columnReorderingAllowed="true"
selectionMode="MULTI">
<actions>
<action id="remove" type="list_remove"/>
<action id="refresh" type="list_refresh" text="msg://ru.ctsg.damdbf.manager.ui.action/Search"
icon="SEARCH"/>
<action id="edit" type="list_edit">
<properties>
<property name="openMode" value="DIALOG"/>
</properties>
</action>
<action id="startAgent" type="list_itemTracking"
text="msg://ru.ctsg.damdbf.manager.ui.action/StartAgentAction"/>
<action id="stopAgent" type="list_itemTracking"
text="msg://ru.ctsg.damdbf.manager.ui.action/StopAgentAction"/>
<action id="clear" text="Очистить" icon="MINUS_CIRCLE"/>
</actions>
<columns resizable="true">
<column property="name" autoWidth="true"/>
<column property="host" autoWidth="true"/>
<column property="status" autoWidth="true"/>
<column property="transitionDate" autoWidth="true"/>
<column property="lastStartDate" autoWidth="true"/>
<column property="description" autoWidth="true" editable="true"/>
</columns>
</dataGrid>
<hbox alignSelf="CENTER">
<simplePagination id="pagination" dataLoader="agentsDl" itemsPerPageVisible="true"
itemsPerPageItems="10, 20, 50, 100" itemsPerPageDefaultValue="10" alignSelf="CENTER"/>
</hbox>
<hbox id="lookupActions" visible="false">
<button id="selectBtn" action="selectAction"/>
<button id="discardBtn" action="discardAction"/>
</hbox>
</layout>
</view>
Контроллер экрана:
@Subscribe("agentsDataGrid.clear")
public void onAgentsDataGridClear(final ActionPerformedEvent event) {
nameFilterComboBox.clear();
statusFilterComboBox.clear();
hostFilterComboBox.clear();
transitionDateFilterDateTimePicker.clear();
lastStartDateFilterDateTimePicker.clear();
agentsDl.load();
}
@Subscribe("agentsDataGrid.refresh")
public void onAgentsDataGridRefresh(final ActionPerformedEvent event) {
reload();
}
private void reload() {
StringBuilder queryBuilder = new StringBuilder("select e from Agent e ");
Map<String, Object> paramsMap = new HashMap<>();
Collection<Object> nameValue = nameFilterComboBox.getTypedValue();
if (nameValue != null && !nameValue.isEmpty()) {
List<Object> names = Collections.singletonList(nameFilterComboBox.getTypedValue());
queryBuilder.append("where e.name in :names");
paramsMap.put("names", names);
}
Collection<AgentStatus> statusValue = statusFilterComboBox.getTypedValue();
if (statusValue != null && !statusValue.isEmpty()) {
List<String> statuses = statusValue.stream()
.map(AgentStatus::getId)
.toList();
queryBuilder.append(" and e.status in :statuses");
paramsMap.put("statuses", statuses);
}
Collection<Object> hostValue = hostFilterComboBox.getTypedValue();
if (hostValue != null && !hostValue.isEmpty()) {
List<Object> hosts = Collections.singletonList(hostFilterComboBox.getTypedValue());
queryBuilder.append(" and e.host in :hosts");
paramsMap.put("hosts", hosts);
}
if (lastStartDateFilterDateTimePicker.getTypedValue() != null) {
Date lastStartDateValue = lastStartDateFilterDateTimePicker.getTypedValue();
queryBuilder.append(" and e.lastStartDate >= :lastStartDate");
paramsMap.put("lastStartDate", lastStartDateValue);
}
if (transitionDateFilterDateTimePicker.getTypedValue() != null) {
Date transitionDateValue = transitionDateFilterDateTimePicker.getTypedValue();
queryBuilder.append(" and e.transitionDate >= :transitionDate");
paramsMap.put("transitionDate", transitionDateValue);
}
String query = queryBuilder.toString();
agentsDl.setQuery(query);
agentsDl.setParameters(paramsMap);
agentsDl.load();
}
Получилось продебажить, все работает)