Added basic query building mechanic for dynamic searching.
This commit is contained in:
parent
90ec1e9b09
commit
85627fb8ad
|
@ -3,9 +3,12 @@ package com.andrewlalis.perfin.control;
|
||||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||||
import com.andrewlalis.perfin.data.AccountRepository;
|
import com.andrewlalis.perfin.data.AccountRepository;
|
||||||
import com.andrewlalis.perfin.data.TransactionRepository;
|
import com.andrewlalis.perfin.data.TransactionRepository;
|
||||||
|
import com.andrewlalis.perfin.data.impl.JdbcDataSource;
|
||||||
import com.andrewlalis.perfin.data.pagination.Page;
|
import com.andrewlalis.perfin.data.pagination.Page;
|
||||||
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||||
import com.andrewlalis.perfin.data.pagination.Sort;
|
import com.andrewlalis.perfin.data.pagination.Sort;
|
||||||
|
import com.andrewlalis.perfin.data.search.JdbcTransactionSearcher;
|
||||||
|
import com.andrewlalis.perfin.data.search.SearchFilter;
|
||||||
import com.andrewlalis.perfin.data.util.DateUtil;
|
import com.andrewlalis.perfin.data.util.DateUtil;
|
||||||
import com.andrewlalis.perfin.data.util.Pair;
|
import com.andrewlalis.perfin.data.util.Pair;
|
||||||
import com.andrewlalis.perfin.model.Account;
|
import com.andrewlalis.perfin.model.Account;
|
||||||
|
@ -22,6 +25,7 @@ import javafx.beans.property.SimpleObjectProperty;
|
||||||
import javafx.beans.value.ObservableValue;
|
import javafx.beans.value.ObservableValue;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.layout.BorderPane;
|
import javafx.scene.layout.BorderPane;
|
||||||
import javafx.scene.layout.HBox;
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
|
@ -30,8 +34,9 @@ import javafx.stage.FileChooser;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||||
|
|
||||||
|
@ -46,6 +51,7 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
public record RouteContext(Long selectedTransactionId) {}
|
public record RouteContext(Long selectedTransactionId) {}
|
||||||
|
|
||||||
@FXML public BorderPane transactionsListBorderPane;
|
@FXML public BorderPane transactionsListBorderPane;
|
||||||
|
@FXML public TextField searchField;
|
||||||
@FXML public AccountSelectionBox filterByAccountComboBox;
|
@FXML public AccountSelectionBox filterByAccountComboBox;
|
||||||
@FXML public VBox transactionsVBox;
|
@FXML public VBox transactionsVBox;
|
||||||
private DataSourcePaginationControls paginationControls;
|
private DataSourcePaginationControls paginationControls;
|
||||||
|
@ -60,33 +66,30 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
paginationControls.setPage(1);
|
paginationControls.setPage(1);
|
||||||
selectedTransaction.set(null);
|
selectedTransaction.set(null);
|
||||||
});
|
});
|
||||||
|
searchField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||||
|
paginationControls.setPage(1);
|
||||||
|
selectedTransaction.set(null);
|
||||||
|
});
|
||||||
|
|
||||||
this.paginationControls = new DataSourcePaginationControls(
|
this.paginationControls = new DataSourcePaginationControls(
|
||||||
transactionsVBox.getChildren(),
|
transactionsVBox.getChildren(),
|
||||||
new DataSourcePaginationControls.PageFetcherFunction() {
|
new DataSourcePaginationControls.PageFetcherFunction() {
|
||||||
@Override
|
@Override
|
||||||
public Page<? extends Node> fetchPage(PageRequest pagination) throws Exception {
|
public Page<? extends Node> fetchPage(PageRequest pagination) throws Exception {
|
||||||
Account accountFilter = filterByAccountComboBox.getValue();
|
JdbcDataSource ds = (JdbcDataSource) Profile.getCurrent().dataSource();
|
||||||
try (var repo = Profile.getCurrent().dataSource().getTransactionRepository()) {
|
try (var conn = ds.getConnection()) {
|
||||||
Page<Transaction> result;
|
JdbcTransactionSearcher searcher = new JdbcTransactionSearcher(conn);
|
||||||
if (accountFilter == null) {
|
return searcher.search(pagination, getCurrentSearchFilters())
|
||||||
result = repo.findAll(pagination);
|
.map(TransactionsViewController.this::makeTile);
|
||||||
} else {
|
|
||||||
result = repo.findAllByAccounts(Set.of(accountFilter.id), pagination);
|
|
||||||
}
|
|
||||||
return result.map(TransactionsViewController.this::makeTile);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getTotalCount() throws Exception {
|
public int getTotalCount() throws Exception {
|
||||||
Account accountFilter = filterByAccountComboBox.getValue();
|
JdbcDataSource ds = (JdbcDataSource) Profile.getCurrent().dataSource();
|
||||||
try (var repo = Profile.getCurrent().dataSource().getTransactionRepository()) {
|
try (var conn = ds.getConnection()) {
|
||||||
if (accountFilter == null) {
|
JdbcTransactionSearcher searcher = new JdbcTransactionSearcher(conn);
|
||||||
return (int) repo.countAll();
|
return (int) searcher.resultCount(getCurrentSearchFilters());
|
||||||
} else {
|
|
||||||
return (int) repo.countAllByAccounts(Set.of(accountFilter.id));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -105,9 +108,7 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
TransactionViewController transactionViewController = detailComponents.second();
|
TransactionViewController transactionViewController = detailComponents.second();
|
||||||
BorderPane transactionDetailView = detailComponents.first();
|
BorderPane transactionDetailView = detailComponents.first();
|
||||||
detailPanel.getChildren().add(transactionDetailView);
|
detailPanel.getChildren().add(transactionDetailView);
|
||||||
selectedTransaction.addListener((observable, oldValue, newValue) -> {
|
selectedTransaction.addListener((observable, oldValue, newValue) -> transactionViewController.setTransaction(newValue));
|
||||||
transactionViewController.setTransaction(newValue);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Clear the transactions when a new profile is loaded.
|
// Clear the transactions when a new profile is loaded.
|
||||||
Profile.whenLoaded(profile -> {
|
Profile.whenLoaded(profile -> {
|
||||||
|
@ -133,16 +134,17 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
|
|
||||||
// If a transaction id is given in the route context, navigate to the page it's on and select it.
|
// If a transaction id is given in the route context, navigate to the page it's on and select it.
|
||||||
if (context instanceof RouteContext ctx && ctx.selectedTransactionId != null) {
|
if (context instanceof RouteContext ctx && ctx.selectedTransactionId != null) {
|
||||||
Profile.getCurrent().dataSource().useRepoAsync(TransactionRepository.class, repo -> {
|
Profile.getCurrent().dataSource().useRepoAsync(
|
||||||
repo.findById(ctx.selectedTransactionId).ifPresent(tx -> {
|
TransactionRepository.class,
|
||||||
|
repo -> repo.findById(ctx.selectedTransactionId).ifPresent(tx -> {
|
||||||
long offset = repo.countAllAfter(tx.id);
|
long offset = repo.countAllAfter(tx.id);
|
||||||
int pageNumber = (int) (offset / paginationControls.getItemsPerPage()) + 1;
|
int pageNumber = (int) (offset / paginationControls.getItemsPerPage()) + 1;
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
paginationControls.setPage(pageNumber);
|
paginationControls.setPage(pageNumber);
|
||||||
selectedTransaction.set(tx);
|
selectedTransaction.set(tx);
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
});
|
);
|
||||||
} else {
|
} else {
|
||||||
paginationControls.setPage(1);
|
paginationControls.setPage(1);
|
||||||
}
|
}
|
||||||
|
@ -180,6 +182,37 @@ public class TransactionsViewController implements RouteSelectionListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<SearchFilter> getCurrentSearchFilters() {
|
||||||
|
List<SearchFilter> filters = new ArrayList<>();
|
||||||
|
if (searchField.getText() != null && !searchField.getText().isBlank()) {
|
||||||
|
var likeTerms = Arrays.stream(searchField.getText().strip().toLowerCase().split("\\s+"))
|
||||||
|
.map(t -> '%'+t+'%')
|
||||||
|
.toList();
|
||||||
|
var builder = new SearchFilter.Builder();
|
||||||
|
List<String> orClauses = new ArrayList<>(likeTerms.size());
|
||||||
|
for (var term : likeTerms) {
|
||||||
|
orClauses.add("LOWER(transaction.description) LIKE ? OR LOWER(sfv.name) LIKE ? OR LOWER(sfc.name) LIKE ?");
|
||||||
|
builder.withArg(term);
|
||||||
|
builder.withArg(term);
|
||||||
|
builder.withArg(term);
|
||||||
|
}
|
||||||
|
builder.where(String.join(" OR ", orClauses));
|
||||||
|
builder.withJoin("LEFT JOIN transaction_vendor sfv ON sfv.id = transaction.vendor_id");
|
||||||
|
builder.withJoin("LEFT JOIN transaction_category sfc ON sfc.id = transaction.category_id");
|
||||||
|
filters.add(builder.build());
|
||||||
|
}
|
||||||
|
if (filterByAccountComboBox.getValue() != null) {
|
||||||
|
Account filteredAccount = filterByAccountComboBox.getValue();
|
||||||
|
var filter = new SearchFilter.Builder()
|
||||||
|
.where("fae.account_id = ?")
|
||||||
|
.withArg(filteredAccount.id)
|
||||||
|
.withJoin("LEFT JOIN account_entry fae ON fae.transaction_id = transaction.id")
|
||||||
|
.build();
|
||||||
|
filters.add(filter);
|
||||||
|
}
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
private TransactionTile makeTile(Transaction transaction) {
|
private TransactionTile makeTile(Transaction transaction) {
|
||||||
var tile = new TransactionTile(transaction);
|
var tile = new TransactionTile(transaction);
|
||||||
tile.setOnMouseClicked(event -> {
|
tile.setOnMouseClicked(event -> {
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package com.andrewlalis.perfin.data.search;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.data.pagination.Page;
|
||||||
|
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An entity searcher will search for entities matching a list of filters.
|
||||||
|
* @param <T> The entity type to search over.
|
||||||
|
*/
|
||||||
|
public interface EntitySearcher<T> {
|
||||||
|
/**
|
||||||
|
* Gets a page of results that match the given filters.
|
||||||
|
* @param pageRequest The page request.
|
||||||
|
* @param filters The filters to apply.
|
||||||
|
* @return A page of results.
|
||||||
|
*/
|
||||||
|
Page<T> search(PageRequest pageRequest, List<SearchFilter> filters);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the number of results that would be returned for a given set of
|
||||||
|
* filters.
|
||||||
|
* @param filters The filters to apply.
|
||||||
|
* @return The number of entities that match.
|
||||||
|
*/
|
||||||
|
long resultCount(List<SearchFilter> filters);
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package com.andrewlalis.perfin.data.search;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.data.pagination.Page;
|
||||||
|
import com.andrewlalis.perfin.data.pagination.PageRequest;
|
||||||
|
import com.andrewlalis.perfin.data.util.Pair;
|
||||||
|
import com.andrewlalis.perfin.data.util.ResultSetMapper;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
public class JdbcEntitySearcher<T> implements EntitySearcher<T> {
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(JdbcEntitySearcher.class);
|
||||||
|
|
||||||
|
private final Connection conn;
|
||||||
|
private final String countExpression;
|
||||||
|
private final String selectExpression;
|
||||||
|
private final ResultSetMapper<T> resultSetMapper;
|
||||||
|
|
||||||
|
public JdbcEntitySearcher(Connection conn, String countExpression, String selectExpression, ResultSetMapper<T> resultSetMapper) {
|
||||||
|
this.conn = conn;
|
||||||
|
this.countExpression = countExpression;
|
||||||
|
this.selectExpression = selectExpression;
|
||||||
|
this.resultSetMapper = resultSetMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pair<String, List<Pair<Integer, Object>>> buildSearchQuery(List<SearchFilter> filters) {
|
||||||
|
if (filters.isEmpty()) return new Pair<>("", Collections.emptyList());
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
List<Pair<Integer, Object>> args = new ArrayList<>();
|
||||||
|
for (var filter : filters) {
|
||||||
|
args.addAll(filter.args());
|
||||||
|
for (var joinClause : filter.joinClauses()) {
|
||||||
|
sb.append(joinClause).append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.append("WHERE\n");
|
||||||
|
for (int i = 0; i < filters.size(); i++) {
|
||||||
|
sb.append(filters.get(i).whereClause());
|
||||||
|
if (i < filters.size() - 1) {
|
||||||
|
sb.append(" AND");
|
||||||
|
}
|
||||||
|
sb.append('\n');
|
||||||
|
}
|
||||||
|
return new Pair<>(sb.toString(), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyArgs(PreparedStatement stmt, List<Pair<Integer, Object>> args) throws SQLException {
|
||||||
|
for (int i = 1; i <= args.size(); i++) {
|
||||||
|
Pair<Integer, Object> arg = args.get(i - 1);
|
||||||
|
if (arg.second() == null) {
|
||||||
|
stmt.setNull(i, arg.first());
|
||||||
|
} else {
|
||||||
|
stmt.setObject(i, arg.second(), arg.first());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Page<T> search(PageRequest pageRequest, List<SearchFilter> filters) {
|
||||||
|
var baseQueryAndArgs = buildSearchQuery(filters);
|
||||||
|
StringBuilder sqlBuilder = new StringBuilder(selectExpression);
|
||||||
|
if (baseQueryAndArgs.first() != null && !baseQueryAndArgs.first().isBlank()) {
|
||||||
|
sqlBuilder.append('\n').append(baseQueryAndArgs.first());
|
||||||
|
}
|
||||||
|
String pagingSql = pageRequest.toSQL();
|
||||||
|
if (pagingSql != null && !pagingSql.isBlank()) {
|
||||||
|
sqlBuilder.append('\n').append(pagingSql);
|
||||||
|
}
|
||||||
|
String sql = sqlBuilder.toString();
|
||||||
|
logger.info("Searching with query:\n{}\nWith arguments: {}", sql, baseQueryAndArgs.second().stream().map(Pair::second).map(Object::toString).collect(Collectors.joining(", ")));
|
||||||
|
try (var stmt = conn.prepareStatement(sql)) {
|
||||||
|
applyArgs(stmt, baseQueryAndArgs.second());
|
||||||
|
ResultSet rs = stmt.executeQuery();
|
||||||
|
List<T> results = new ArrayList<>(pageRequest.size());
|
||||||
|
while (rs.next() && results.size() < pageRequest.size()) {
|
||||||
|
results.add(resultSetMapper.map(rs));
|
||||||
|
}
|
||||||
|
return new Page<>(results, pageRequest);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Search failed.", e);
|
||||||
|
return new Page<>(Collections.emptyList(), pageRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long resultCount(List<SearchFilter> filters) {
|
||||||
|
var baseQueryAndArgs = buildSearchQuery(filters);
|
||||||
|
String sql = countExpression + "\n" + baseQueryAndArgs.first();
|
||||||
|
try (var stmt = conn.prepareStatement(sql)) {
|
||||||
|
applyArgs(stmt, baseQueryAndArgs.second());
|
||||||
|
ResultSet rs = stmt.executeQuery();
|
||||||
|
if (!rs.next()) throw new SQLException("No count result.");
|
||||||
|
return rs.getLong(1);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
logger.error("Failed to get search result count.", e);
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Builder {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package com.andrewlalis.perfin.data.search;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||||
|
import com.andrewlalis.perfin.model.Transaction;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Currency;
|
||||||
|
|
||||||
|
public class JdbcTransactionSearcher extends JdbcEntitySearcher<Transaction> {
|
||||||
|
public JdbcTransactionSearcher(Connection conn) {
|
||||||
|
super(
|
||||||
|
conn,
|
||||||
|
"SELECT COUNT(transaction.id) FROM transaction",
|
||||||
|
"SELECT transaction.* FROM transaction",
|
||||||
|
JdbcTransactionSearcher::parseResultSet
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Transaction parseResultSet(ResultSet rs) throws SQLException {
|
||||||
|
long id = rs.getLong(1);
|
||||||
|
LocalDateTime timestamp = DbUtil.utcLDTFromTimestamp(rs.getTimestamp(2));
|
||||||
|
BigDecimal amount = rs.getBigDecimal(3);
|
||||||
|
Currency currency = Currency.getInstance(rs.getString(4));
|
||||||
|
String description = rs.getString(5);
|
||||||
|
Long vendorId = rs.getLong(6);
|
||||||
|
if (rs.wasNull()) vendorId = null;
|
||||||
|
Long categoryId = rs.getLong(7);
|
||||||
|
if (rs.wasNull()) categoryId = null;
|
||||||
|
return new Transaction(id, timestamp, amount, currency, description, vendorId, categoryId);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package com.andrewlalis.perfin.data.search;
|
||||||
|
|
||||||
|
import com.andrewlalis.perfin.data.util.DbUtil;
|
||||||
|
import com.andrewlalis.perfin.data.util.Pair;
|
||||||
|
|
||||||
|
import java.sql.Types;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface SearchFilter {
|
||||||
|
String whereClause();
|
||||||
|
List<Pair<Integer, Object>> args();
|
||||||
|
default List<String> joinClauses() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
record Impl(String whereClause, List<Pair<Integer, Object>> args, List<String> joinClauses) implements SearchFilter {}
|
||||||
|
|
||||||
|
class Builder {
|
||||||
|
private String whereClause;
|
||||||
|
private List<Pair<Integer, Object>> args = new ArrayList<>();
|
||||||
|
private List<String> joinClauses = new ArrayList<>();
|
||||||
|
|
||||||
|
public Builder where(String clause) {
|
||||||
|
this.whereClause = clause;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withArg(int sqlType, Object value) {
|
||||||
|
args.add(new Pair<>(sqlType, value));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withArg(int value) {
|
||||||
|
return withArg(Types.INTEGER, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withArg(long value) {
|
||||||
|
return withArg(Types.BIGINT, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withArg(String value) {
|
||||||
|
return withArg(Types.VARCHAR, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withArg(LocalDateTime utcTimestamp) {
|
||||||
|
return withArg(Types.TIMESTAMP, DbUtil.timestampFromUtcLDT(utcTimestamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Builder withJoin(String joinClause) {
|
||||||
|
joinClauses.add(joinClause);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchFilter build() {
|
||||||
|
return new Impl(whereClause, args, joinClauses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,6 +6,7 @@
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
<?import javafx.scene.control.ScrollPane?>
|
<?import javafx.scene.control.ScrollPane?>
|
||||||
<?import javafx.scene.layout.*?>
|
<?import javafx.scene.layout.*?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
<BorderPane xmlns="http://javafx.com/javafx"
|
<BorderPane xmlns="http://javafx.com/javafx"
|
||||||
xmlns:fx="http://javafx.com/fxml"
|
xmlns:fx="http://javafx.com/fxml"
|
||||||
fx:controller="com.andrewlalis.perfin.control.TransactionsViewController"
|
fx:controller="com.andrewlalis.perfin.control.TransactionsViewController"
|
||||||
|
@ -20,7 +21,8 @@
|
||||||
<HBox>
|
<HBox>
|
||||||
<BorderPane fx:id="transactionsListBorderPane" HBox.hgrow="ALWAYS">
|
<BorderPane fx:id="transactionsListBorderPane" HBox.hgrow="ALWAYS">
|
||||||
<top>
|
<top>
|
||||||
<HBox styleClass="std-padding,std-spacing">
|
<HBox styleClass="padding-extra,std-spacing">
|
||||||
|
<TextField fx:id="searchField" promptText="Search"/>
|
||||||
<PropertiesPane hgap="5" vgap="5">
|
<PropertiesPane hgap="5" vgap="5">
|
||||||
<Label text="Filter by Account"/>
|
<Label text="Filter by Account"/>
|
||||||
<AccountSelectionBox fx:id="filterByAccountComboBox" allowNone="true" showBalance="false"/>
|
<AccountSelectionBox fx:id="filterByAccountComboBox" allowNone="true" showBalance="false"/>
|
||||||
|
|
Loading…
Reference in New Issue