Added pages for viewing and editing vendors, and refactored validation to support async validators.
This commit is contained in:
parent
ae2713dbd0
commit
77291ba724
|
@ -91,6 +91,8 @@ public class PerfinApp extends Application {
|
|||
router.map("edit-transaction", PerfinApp.class.getResource("/edit-transaction.fxml"));
|
||||
router.map("create-balance-record", PerfinApp.class.getResource("/create-balance-record.fxml"));
|
||||
router.map("balance-record", PerfinApp.class.getResource("/balance-record-view.fxml"));
|
||||
router.map("vendors", PerfinApp.class.getResource("/vendors-view.fxml"));
|
||||
router.map("edit-vendor", PerfinApp.class.getResource("/edit-vendor.fxml"));
|
||||
|
||||
// Help pages.
|
||||
helpRouter.map("home", PerfinApp.class.getResource("/help-pages/home.fxml"));
|
||||
|
|
|
@ -11,6 +11,7 @@ import com.andrewlalis.perfin.model.Profile;
|
|||
import com.andrewlalis.perfin.view.component.FileSelectionArea;
|
||||
import com.andrewlalis.perfin.view.component.PropertiesPane;
|
||||
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
||||
import com.andrewlalis.perfin.view.component.validation.ValidationFunction;
|
||||
import com.andrewlalis.perfin.view.component.validation.ValidationResult;
|
||||
import com.andrewlalis.perfin.view.component.validation.validators.CurrencyAmountValidator;
|
||||
import javafx.application.Platform;
|
||||
|
@ -39,7 +40,7 @@ public class CreateBalanceRecordController implements RouteSelectionListener {
|
|||
private Account account;
|
||||
|
||||
@FXML public void initialize() {
|
||||
var timestampValid = new ValidationApplier<String>(input -> {
|
||||
var timestampValid = new ValidationApplier<>((ValidationFunction<String>) input -> {
|
||||
try {
|
||||
DateUtil.DEFAULT_DATETIME_FORMAT.parse(input);
|
||||
return ValidationResult.valid();
|
||||
|
|
|
@ -60,6 +60,7 @@ public class EditTransactionController implements RouteSelectionListener {
|
|||
@FXML public AccountSelectionBox creditAccountSelector;
|
||||
|
||||
@FXML public ComboBox<String> vendorComboBox;
|
||||
@FXML public Hyperlink vendorsHyperlink;
|
||||
@FXML public ComboBox<String> categoryComboBox;
|
||||
@FXML public ComboBox<String> tagsComboBox;
|
||||
@FXML public Button addTagButton;
|
||||
|
@ -89,6 +90,8 @@ public class EditTransactionController implements RouteSelectionListener {
|
|||
).validatedInitially().attach(descriptionField, descriptionField.textProperty());
|
||||
var linkedAccountsValid = initializeLinkedAccountsValidationUi();
|
||||
initializeTagSelectionUi();
|
||||
// Setup hyperlinks.
|
||||
vendorsHyperlink.setOnAction(event -> router.navigate("vendors"));
|
||||
|
||||
var formValid = timestampValid.and(amountValid).and(descriptionValid).and(linkedAccountsValid);
|
||||
saveButton.disableProperty().bind(formValid.not());
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
package com.andrewlalis.perfin.control;
|
||||
|
||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||
import com.andrewlalis.perfin.data.DataSource;
|
||||
import com.andrewlalis.perfin.data.TransactionVendorRepository;
|
||||
import com.andrewlalis.perfin.model.Profile;
|
||||
import com.andrewlalis.perfin.model.TransactionVendor;
|
||||
import com.andrewlalis.perfin.view.component.validation.ValidationApplier;
|
||||
import com.andrewlalis.perfin.view.component.validation.validators.PredicateValidator;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.TextArea;
|
||||
import javafx.scene.control.TextField;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||
|
||||
public class EditVendorController implements RouteSelectionListener {
|
||||
private TransactionVendor vendor;
|
||||
|
||||
@FXML public TextField nameField;
|
||||
@FXML public TextArea descriptionField;
|
||||
@FXML public Button saveButton;
|
||||
|
||||
@FXML public void initialize() {
|
||||
var nameValid = new ValidationApplier<>(new PredicateValidator<String>()
|
||||
.addTerminalPredicate(s -> s != null && !s.isBlank(), "Name is required.")
|
||||
.addPredicate(s -> s.strip().length() <= TransactionVendor.NAME_MAX_LENGTH, "Name is too long.")
|
||||
// A predicate that prevents duplicate names.
|
||||
.addAsyncPredicate(
|
||||
s -> {
|
||||
if (Profile.getCurrent() == null) return CompletableFuture.completedFuture(false);
|
||||
return Profile.getCurrent().dataSource().mapRepoAsync(
|
||||
TransactionVendorRepository.class,
|
||||
repo -> {
|
||||
var vendorByName = repo.findByName(s).orElse(null);
|
||||
if (this.vendor != null) {
|
||||
return this.vendor.equals(vendorByName) || vendorByName == null;
|
||||
}
|
||||
return vendorByName == null;
|
||||
}
|
||||
);
|
||||
},
|
||||
"Vendor with this name already exists."
|
||||
)
|
||||
).validatedInitially().attachToTextField(nameField);
|
||||
var descriptionValid = new ValidationApplier<>(new PredicateValidator<String>()
|
||||
.addPredicate(
|
||||
s -> s == null || s.strip().length() <= TransactionVendor.DESCRIPTION_MAX_LENGTH,
|
||||
"Description is too long."
|
||||
)
|
||||
).validatedInitially().attach(descriptionField, descriptionField.textProperty());
|
||||
|
||||
var formValid = nameValid.and(descriptionValid);
|
||||
saveButton.disableProperty().bind(formValid.not());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRouteSelected(Object context) {
|
||||
if (context instanceof TransactionVendor tv) {
|
||||
this.vendor = tv;
|
||||
nameField.setText(vendor.getName());
|
||||
descriptionField.setText(vendor.getDescription());
|
||||
} else {
|
||||
nameField.setText(null);
|
||||
descriptionField.setText(null);
|
||||
}
|
||||
}
|
||||
|
||||
@FXML public void save() {
|
||||
String name = nameField.getText().strip();
|
||||
String description = descriptionField.getText() == null ? null : descriptionField.getText().strip();
|
||||
DataSource ds = Profile.getCurrent().dataSource();
|
||||
if (vendor != null) {
|
||||
ds.useRepo(TransactionVendorRepository.class, repo -> repo.update(vendor.id, name, description));
|
||||
} else {
|
||||
ds.useRepo(TransactionVendorRepository.class, repo -> {
|
||||
if (description == null || description.isEmpty()) {
|
||||
repo.insert(name);
|
||||
} else {
|
||||
repo.insert(name, description);
|
||||
}
|
||||
});
|
||||
}
|
||||
router.replace("vendors");
|
||||
}
|
||||
|
||||
@FXML public void cancel() {
|
||||
router.navigateBackAndClear();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package com.andrewlalis.perfin.control;
|
||||
|
||||
import com.andrewlalis.javafx_scene_router.RouteSelectionListener;
|
||||
import com.andrewlalis.perfin.data.TransactionVendorRepository;
|
||||
import com.andrewlalis.perfin.model.Profile;
|
||||
import com.andrewlalis.perfin.model.TransactionVendor;
|
||||
import com.andrewlalis.perfin.view.BindingUtil;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.andrewlalis.perfin.PerfinApp.router;
|
||||
|
||||
public class VendorsViewController implements RouteSelectionListener {
|
||||
@FXML public VBox vendorsVBox;
|
||||
private final ObservableList<TransactionVendor> vendors = FXCollections.observableArrayList();
|
||||
|
||||
@FXML public void initialize() {
|
||||
BindingUtil.mapContent(vendorsVBox.getChildren(), vendors, this::buildVendorTile);
|
||||
}
|
||||
|
||||
private Node buildVendorTile(TransactionVendor transactionVendor) {
|
||||
BorderPane pane = new BorderPane();
|
||||
pane.getStyleClass().addAll("tile", "std-spacing");
|
||||
pane.setOnMouseClicked(event -> router.navigate("edit-vendor", transactionVendor));
|
||||
|
||||
Label nameLabel = new Label(transactionVendor.getName());
|
||||
nameLabel.getStyleClass().addAll("bold-text");
|
||||
Label descriptionLabel = new Label(transactionVendor.getDescription());
|
||||
descriptionLabel.setWrapText(true);
|
||||
VBox contentVBox = new VBox(nameLabel, descriptionLabel);
|
||||
contentVBox.getStyleClass().addAll("std-spacing");
|
||||
pane.setCenter(contentVBox);
|
||||
BorderPane.setAlignment(contentVBox, Pos.TOP_LEFT);
|
||||
|
||||
Button removeButton = new Button("Remove");
|
||||
removeButton.setOnAction(event -> {
|
||||
boolean confirm = Popups.confirm(removeButton, "Are you sure you want to remove this vendor? Any transactions with assigned to this vendor will have their vendor field cleared. This cannot be undone.");
|
||||
if (confirm) {
|
||||
Profile.getCurrent().dataSource().useRepo(TransactionVendorRepository.class, repo -> {
|
||||
repo.deleteById(transactionVendor.id);
|
||||
});
|
||||
refreshVendors();
|
||||
}
|
||||
});
|
||||
pane.setRight(removeButton);
|
||||
|
||||
return pane;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRouteSelected(Object context) {
|
||||
refreshVendors();
|
||||
}
|
||||
|
||||
@FXML public void addVendor() {
|
||||
router.navigate("edit-vendor");
|
||||
}
|
||||
|
||||
private void refreshVendors() {
|
||||
Profile.getCurrent().dataSource().useRepoAsync(TransactionVendorRepository.class, repo -> {
|
||||
final List<TransactionVendor> vendors = repo.findAll();
|
||||
Platform.runLater(() -> {
|
||||
this.vendors.clear();
|
||||
this.vendors.addAll(vendors);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -11,5 +11,6 @@ public interface TransactionVendorRepository extends Repository, AutoCloseable {
|
|||
List<TransactionVendor> findAll();
|
||||
long insert(String name, String description);
|
||||
long insert(String name);
|
||||
void update(long id, String name, String description);
|
||||
void deleteById(long id);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import java.sql.Connection;
|
|||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
public record JdbcTransactionVendorRepository(Connection conn) implements TransactionVendorRepository {
|
||||
|
@ -58,6 +59,29 @@ public record JdbcTransactionVendorRepository(Connection conn) implements Transa
|
|||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update(long id, String name, String description) {
|
||||
DbUtil.doTransaction(conn, () -> {
|
||||
TransactionVendor vendor = findById(id).orElseThrow();
|
||||
if (!vendor.getName().equals(name)) {
|
||||
DbUtil.updateOne(
|
||||
conn,
|
||||
"UPDATE transaction_vendor SET name = ? WHERE id = ?",
|
||||
name,
|
||||
id
|
||||
);
|
||||
}
|
||||
if (!Objects.equals(vendor.getDescription(), description)) {
|
||||
DbUtil.updateOne(
|
||||
conn,
|
||||
"UPDATE transaction_vendor SET description = ? WHERE id = ?",
|
||||
description,
|
||||
id
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteById(long id) {
|
||||
DbUtil.update(conn, "DELETE FROM transaction_vendor WHERE id = ?", List.of(id));
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package com.andrewlalis.perfin.view.component.validation;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface AsyncValidationFunction<T> {
|
||||
CompletableFuture<ValidationResult> validate(T input);
|
||||
}
|
|
@ -1,24 +1,40 @@
|
|||
package com.andrewlalis.perfin.view.component.validation;
|
||||
|
||||
import com.andrewlalis.perfin.view.component.validation.decorators.FieldSubtextDecorator;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.BooleanExpression;
|
||||
import javafx.beans.property.Property;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.TextField;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Fluent interface for applying a validator to one or more controls.
|
||||
* @param <T> The value type.
|
||||
*/
|
||||
public class ValidationApplier<T> {
|
||||
private final ValidationFunction<T> validator;
|
||||
private final AsyncValidationFunction<T> validator;
|
||||
private ValidationDecorator decorator = new FieldSubtextDecorator();
|
||||
private boolean validateInitially = false;
|
||||
|
||||
public ValidationApplier(ValidationFunction<T> validator) {
|
||||
this.validator = input -> CompletableFuture.completedFuture(validator.validate(input));
|
||||
}
|
||||
|
||||
public ValidationApplier(AsyncValidationFunction<T> validator) {
|
||||
this.validator = validator;
|
||||
}
|
||||
|
||||
public static <T> ValidationApplier<T> of(ValidationFunction<T> validator) {
|
||||
return new ValidationApplier<>(validator);
|
||||
}
|
||||
|
||||
public static <T> ValidationApplier<T> ofAsync(AsyncValidationFunction<T> validator) {
|
||||
return new ValidationApplier<>(validator);
|
||||
}
|
||||
|
||||
public ValidationApplier<T> decoratedWith(ValidationDecorator decorator) {
|
||||
this.decorator = decorator;
|
||||
return this;
|
||||
|
@ -29,24 +45,47 @@ public class ValidationApplier<T> {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the configured validator and decorator to a node, so that when
|
||||
* the node's specified valueProperty changes, the validator will be called
|
||||
* and if the new value is invalid, the decorator will update the UI to
|
||||
* show the message(s) to the user.
|
||||
* @param node The node to attach to.
|
||||
* @param valueProperty The property to listen for changes and validate on.
|
||||
* @param triggerProperties Additional properties that, when changed, can
|
||||
* trigger validation.
|
||||
* @return A boolean expression that tells whether the given valueProperty
|
||||
* is valid at any given time.
|
||||
*/
|
||||
public BooleanExpression attach(Node node, Property<T> valueProperty, Property<?>... triggerProperties) {
|
||||
BooleanExpression validProperty = BooleanExpression.booleanExpression(
|
||||
valueProperty.map(value -> validator.validate(value).isValid())
|
||||
);
|
||||
final SimpleBooleanProperty validProperty = new SimpleBooleanProperty();
|
||||
valueProperty.addListener((observable, oldValue, newValue) -> {
|
||||
ValidationResult result = validator.validate(newValue);
|
||||
validProperty.set(false); // Always set valid to false before we start validation.
|
||||
validator.validate(newValue)
|
||||
.thenAccept(result -> Platform.runLater(() -> {
|
||||
validProperty.set(result.isValid());
|
||||
decorator.decorate(node, result);
|
||||
}));
|
||||
});
|
||||
for (Property<?> influencingProperty : triggerProperties) {
|
||||
influencingProperty.addListener((observable, oldValue, newValue) -> {
|
||||
ValidationResult result = validator.validate(valueProperty.getValue());
|
||||
validProperty.set(false); // Always set valid to false before we start validation.
|
||||
validator.validate(valueProperty.getValue())
|
||||
.thenAccept(result -> Platform.runLater(() -> {
|
||||
validProperty.set(result.isValid());
|
||||
decorator.decorate(node, result);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
if (validateInitially) {
|
||||
// Call the decorator once to perform validation right away.
|
||||
decorator.decorate(node, validator.validate(valueProperty.getValue()));
|
||||
validProperty.set(false); // Always set valid to false before we start validation.
|
||||
validator.validate(valueProperty.getValue())
|
||||
.thenAccept(result -> Platform.runLater(() -> {
|
||||
validProperty.set(result.isValid());
|
||||
decorator.decorate(node, result);
|
||||
}));
|
||||
}
|
||||
return validProperty;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
package com.andrewlalis.perfin.view.component.validation.validators;
|
||||
|
||||
import com.andrewlalis.perfin.view.component.validation.ValidationFunction;
|
||||
import com.andrewlalis.perfin.view.component.validation.AsyncValidationFunction;
|
||||
import com.andrewlalis.perfin.view.component.validation.ValidationResult;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
|
@ -12,32 +16,73 @@ import java.util.function.Function;
|
|||
* determine if it's valid. If invalid, a message is added.
|
||||
* @param <T> The value type.
|
||||
*/
|
||||
public class PredicateValidator<T> implements ValidationFunction<T> {
|
||||
private record ValidationStep<T>(Function<T, Boolean> predicate, String message, boolean terminal) {}
|
||||
public class PredicateValidator<T> implements AsyncValidationFunction<T> {
|
||||
private static final Logger logger = LoggerFactory.getLogger(PredicateValidator.class);
|
||||
|
||||
private record ValidationStep<T>(Function<T, CompletableFuture<Boolean>> predicate, String message, boolean terminal) {}
|
||||
|
||||
private final List<ValidationStep<T>> steps = new ArrayList<>();
|
||||
|
||||
public PredicateValidator<T> addPredicate(Function<T, Boolean> predicate, String errorMessage) {
|
||||
steps.add(new ValidationStep<>(predicate, errorMessage, false));
|
||||
private PredicateValidator<T> addPredicate(Function<T, Boolean> predicate, String errorMessage, boolean terminal) {
|
||||
steps.add(new ValidationStep<>(
|
||||
v -> CompletableFuture.completedFuture(predicate.apply(v)),
|
||||
errorMessage,
|
||||
terminal
|
||||
));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PredicateValidator<T> addTerminalPredicate(Function<T, Boolean> predicate, String errorMessage) {
|
||||
steps.add(new ValidationStep<>(predicate, errorMessage, true));
|
||||
private PredicateValidator<T> addAsyncPredicate(Function<T, CompletableFuture<Boolean>> asyncPredicate, String errorMessage, boolean terminal) {
|
||||
steps.add(new ValidationStep<>(asyncPredicate, errorMessage, terminal));
|
||||
return this;
|
||||
}
|
||||
|
||||
public PredicateValidator<T> addPredicate(Function<T, Boolean> predicate, String errorMessage) {
|
||||
return addPredicate(predicate, errorMessage, false);
|
||||
}
|
||||
|
||||
public PredicateValidator<T> addAsyncPredicate(Function<T, CompletableFuture<Boolean>> asyncPredicate, String errorMessage) {
|
||||
return addAsyncPredicate(asyncPredicate, errorMessage, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a terminal predicate, that is, if the given boolean function
|
||||
* evaluates to false, then no further predicates are evaluated.
|
||||
* @param predicate The predicate function.
|
||||
* @param errorMessage The error message to display if the predicate
|
||||
* evaluates to false for a given value.
|
||||
* @return A reference to the validator, for method chaining.
|
||||
*/
|
||||
public PredicateValidator<T> addTerminalPredicate(Function<T, Boolean> predicate, String errorMessage) {
|
||||
return addPredicate(predicate, errorMessage, true);
|
||||
}
|
||||
|
||||
public PredicateValidator<T> addTerminalAsyncPredicate(Function<T, CompletableFuture<Boolean>> asyncPredicate, String errorMessage) {
|
||||
return addAsyncPredicate(asyncPredicate, errorMessage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidationResult validate(T input) {
|
||||
public CompletableFuture<ValidationResult> validate(T input) {
|
||||
CompletableFuture<ValidationResult> cf = new CompletableFuture<>();
|
||||
Thread.ofVirtual().start(() -> {
|
||||
List<String> messages = new ArrayList<>();
|
||||
for (var step : steps) {
|
||||
if (!step.predicate().apply(input)) {
|
||||
try {
|
||||
boolean success = step.predicate().apply(input).get();
|
||||
if (!success) {
|
||||
messages.add(step.message());
|
||||
if (step.terminal()) {
|
||||
return new ValidationResult(messages);
|
||||
cf.complete(new ValidationResult(messages));
|
||||
return; // Exit if this is a terminal step and it failed.
|
||||
}
|
||||
}
|
||||
}
|
||||
return new ValidationResult(messages);
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
logger.error("Applying a predicate to input failed.", e);
|
||||
cf.completeExceptionally(e);
|
||||
}
|
||||
}
|
||||
cf.complete(new ValidationResult(messages));
|
||||
});
|
||||
return cf;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
|
||||
<VBox>
|
||||
<Label text="Vendor" labelFor="${vendorComboBox}" styleClass="bold-text"/>
|
||||
<Hyperlink text="Manage vendors" styleClass="small-font"/>
|
||||
<Hyperlink fx:id="vendorsHyperlink" text="Manage vendors" styleClass="small-font"/>
|
||||
</VBox>
|
||||
<ComboBox fx:id="vendorComboBox" editable="true" maxWidth="Infinity"/>
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import com.andrewlalis.perfin.view.component.PropertiesPane?>
|
||||
<?import javafx.scene.control.*?>
|
||||
<?import javafx.scene.layout.*?>
|
||||
<BorderPane xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="com.andrewlalis.perfin.control.EditVendorController"
|
||||
>
|
||||
<top>
|
||||
<Label text="Edit Vendor" styleClass="bold-text,large-font,std-padding"/>
|
||||
</top>
|
||||
<center>
|
||||
<VBox>
|
||||
<PropertiesPane hgap="5" vgap="5" styleClass="std-padding" maxWidth="500">
|
||||
<columnConstraints>
|
||||
<ColumnConstraints hgrow="NEVER" halignment="LEFT" minWidth="150"/>
|
||||
<ColumnConstraints hgrow="ALWAYS" halignment="RIGHT"/>
|
||||
</columnConstraints>
|
||||
|
||||
<Label text="Name" labelFor="${nameField}"/>
|
||||
<TextField fx:id="nameField"/>
|
||||
|
||||
<Label text="Description" labelFor="${descriptionField}"/>
|
||||
<TextArea fx:id="descriptionField" wrapText="true"/>
|
||||
</PropertiesPane>
|
||||
<HBox styleClass="std-padding,std-spacing">
|
||||
<Button text="Save" fx:id="saveButton" onAction="#save"/>
|
||||
<Button text="Cancel" onAction="#cancel"/>
|
||||
</HBox>
|
||||
</VBox>
|
||||
</center>
|
||||
</BorderPane>
|
|
@ -0,0 +1,27 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.ScrollPane?>
|
||||
<?import javafx.scene.layout.BorderPane?>
|
||||
<?import javafx.scene.layout.HBox?>
|
||||
<?import javafx.scene.layout.VBox?>
|
||||
<?import javafx.scene.text.Text?>
|
||||
<BorderPane xmlns="http://javafx.com/javafx"
|
||||
xmlns:fx="http://javafx.com/fxml"
|
||||
fx:controller="com.andrewlalis.perfin.control.VendorsViewController"
|
||||
>
|
||||
<top>
|
||||
<Label text="Vendors" styleClass="large-font,bold-text,std-padding"/>
|
||||
</top>
|
||||
<center>
|
||||
<VBox>
|
||||
<HBox styleClass="std-padding,std-spacing" VBox.vgrow="NEVER">
|
||||
<Button text="Add Vendor" onAction="#addVendor"/>
|
||||
</HBox>
|
||||
<ScrollPane styleClass="tile-container-scroll" VBox.vgrow="ALWAYS">
|
||||
<VBox fx:id="vendorsVBox" styleClass="tile-container"/>
|
||||
</ScrollPane>
|
||||
</VBox>
|
||||
</center>
|
||||
</BorderPane>
|
Loading…
Reference in New Issue