Discover Repeat collections and compatible Components in both hosts

This commit is contained in:
2026-11-13 14:47:21 +01:00
parent 0300062fb6
commit 0d14ac14e0
28 changed files with 969 additions and 44 deletions
@@ -24,6 +24,9 @@ final class BindingEditor extends VBox {
pane = field("pane"),
component = field("component"),
collection = field("collection");
private final ComboBox<BindingMetadata.RepeatOption> repeatCollections = new ComboBox<>(),
repeatComponents = new ComboBox<>(),
repeatPanes = new ComboBox<>();
private final ComboBox<BindingMetadata.Computed> methods = new ComboBox<>();
private final BindingDocument.Kind kind;
private final ComboBox<BindingMetadata.Converter> converters = new ComboBox<>();
@@ -36,7 +39,7 @@ final class BindingEditor extends VBox {
remove = new Button("Remove binding");
private String source = "";
private Integer selected;
private boolean loading, writable, active, followingCanvas;
private boolean loading, writable, active, followingCanvas, updatingChoices;
private final Consumer<String> navigate;
BindingEditor(
@@ -66,7 +69,7 @@ final class BindingEditor extends VBox {
remove.setText("Remove Computed binding");
}
if (repeat) {
inspect.setText("Validate Repeat bindings");
inspect.setText("Discover / Validate Repeat");
fresh.setText("New Repeat binding");
apply.setText("Apply Repeat binding");
remove.setText("Remove Repeat binding");
@@ -141,8 +144,34 @@ final class BindingEditor extends VBox {
field.textProperty().addListener((o, a, b) -> methods.getItems().clear());
}
if (repeat) {
grid.addRow(0, new Label("Pane (fx:id)"), pane, new Label("Collection path"), collection);
grid.addRow(1, new Label("Component class"), component);
repeatCollections.setId("repeat-collections");
repeatComponents.setId("repeat-components");
repeatPanes.setId("repeat-panes");
repeatCollections.setPrefWidth(420);
repeatComponents.setPrefWidth(680);
repeatPanes.setPrefWidth(420);
repeatCollections.setOnAction(
e -> {
var v = repeatCollections.getValue();
if (v != null && !updatingChoices) {
collection.setText(v.name());
discover.accept(values());
}
});
repeatComponents.setOnAction(
e -> {
var v = repeatComponents.getValue();
if (v != null && !updatingChoices) component.setText(v.name());
});
repeatPanes.setOnAction(
e -> {
var v = repeatPanes.getValue();
if (v != null && !updatingChoices) pane.setText(v.name());
});
collection.textProperty().addListener((o, a, b) -> repeatComponents.getItems().clear());
grid.addRow(0, new Label("Pane (fx:id)"), pane, repeatPanes);
grid.addRow(1, new Label("Collection path"), collection, repeatCollections);
grid.addRow(2, new Label("Component class"), component, repeatComponents);
}
getChildren()
.addAll(
@@ -209,6 +238,9 @@ final class BindingEditor extends VBox {
if (source.equals(text) && writable == editable) return;
converters.getItems().clear();
methods.getItems().clear();
repeatCollections.getItems().clear();
repeatComponents.getItems().clear();
repeatPanes.getItems().clear();
source = text;
writable = editable;
loading = true;
@@ -276,16 +308,25 @@ final class BindingEditor extends VBox {
if (kind == BindingDocument.Kind.REPEAT) {
try {
var errors = new BindingDocument(source, kind).errors();
status.setText(
provenance
+ "\n"
+ (errors.isEmpty() ? "No structural binding errors." : String.join("\n", errors))
+ "\n"
+ "Collection and Component compatibility have not been checked. Render drafts to"
+ " test them.");
if (!errors.isEmpty()) status.setText(status.getText() + "\n" + String.join("\n", errors));
} catch (Exception error) {
status.setText("Validation unavailable: " + error.getMessage());
}
status.setText(
status.getText()
+ "\n"
+ "Compatibility uses declarations; construction and runtime values remain preview"
+ " checks.");
if (requested != null && values().equals(requested)) {
updatingChoices = true;
try {
repeatCollections.getItems().setAll(metadata.collections());
repeatComponents.getItems().setAll(metadata.components());
repeatPanes.getItems().setAll(metadata.panes());
} finally {
updatingChoices = false;
}
}
return;
}
if (requested == null || !values().equals(requested)) return;
@@ -303,6 +344,17 @@ final class BindingEditor extends VBox {
}
}
void invalidateDiscovery() {
updatingChoices = true;
try {
repeatCollections.getItems().clear();
repeatComponents.getItems().clear();
repeatPanes.getItems().clear();
} finally {
updatingChoices = false;
}
}
void error(String message) {
status.setText(message);
}
@@ -682,6 +682,7 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
validationPreview = false;
bindingWorkflow.cancel();
bindingBrowser.invalidate(message);
repeatEditor.invalidateDiscovery();
}
private void editBinding(BindingEditor.Intent intent) {
@@ -744,7 +745,11 @@ public final class DesignerPane extends BorderPane implements AutoCloseable {
.bindingDetails(
last,
captured,
values == null ? "" : values.path(),
values == null
? ""
: values.pane().isEmpty() && values.collection().isEmpty()
? values.path()
: values.collection(),
values == null ? "both" : values.direction(),
values == null ? "" : values.control(),
values == null ? "" : values.property())
@@ -727,14 +727,105 @@ public final class ProjectionHostChecks {
java.nio.file.Path.of(document.snapshot().toCompletableFuture().get().document()))
.equals(changed),
"Repeat save changed source");
click(pane, "Validate Repeat bindings");
selectRepeat(pane, "otherVisits");
click(pane, "Discover / Validate Repeat");
await(
() ->
fx(
() ->
((Label) pane.lookup("#repeat-status"))
.getText()
.contains("Collection and Component compatibility")));
.contains("Compatibility uses declarations")));
await(() -> fx(() -> !((ComboBox<?>) pane.lookup("#repeat-components")).getItems().isEmpty()));
fx(
() -> {
var choices =
(ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.RepeatOption>)
pane.lookup("#repeat-components");
check(
choices.getItems().stream()
.anyMatch(
c ->
c.name().equals("fixture.components.AddressForm")
&& c.status().equals("COMPATIBLE")),
"Compatible Component choice absent");
check(
choices.getItems().stream()
.anyMatch(
c ->
c.name().equals("fixture.components.WrongRepeatCard")
&& c.status().equals("INCOMPATIBLE")),
"Incompatible Component not explained");
var panes =
(ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.RepeatOption>)
pane.lookup("#repeat-panes");
check(
panes.getItems().stream()
.anyMatch(c -> c.name().equals("action") && c.status().equals("INCOMPATIBLE")),
"Non-Pane target accepted");
check(
((TextField) pane.lookup("#repeat-component"))
.getText()
.equals("fixture.components.VisitRow"),
"Discovery replaced explicit Component");
var collections =
(ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.RepeatOption>)
pane.lookup("#repeat-collections");
collections.setValue(
collections.getItems().stream()
.filter(c -> c.name().equals("visits"))
.findFirst()
.orElseThrow());
return null;
});
await(() -> fx(() -> !((ComboBox<?>) pane.lookup("#repeat-components")).getItems().isEmpty()));
check(documentText(document).equals(changed), "Repeat discovery changed source");
fx(
() -> {
var choices =
(ComboBox<de.neitzel.fxdesigner.protocol.BindingMetadata.RepeatOption>)
pane.lookup("#repeat-components");
choices.setValue(
choices.getItems().stream()
.filter(c -> c.name().equals("fixture.components.WrongRepeatCard"))
.findFirst()
.orElseThrow());
return null;
});
check(documentText(document).equals(changed), "Component choice applied without review");
click(pane, "Apply Repeat binding");
String mismatched =
changed.replace(
"component=\"fixture.components.VisitRow\"",
"component=\"fixture.components.WrongRepeatCard\"");
await(() -> documentText(document).equals(mismatched));
await(() -> fx(() -> source(pane).getText().equals(mismatched)));
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()
.get();
click(pane, "Render drafts");
await(
() ->
fx(
() ->
((Label) pane.lookup("#repeat-status"))
.getText()
.contains("incompatible with Component Model")));
await(
() ->
fx(
() ->
pane.lookupAll(".label").stream()
.anyMatch(
n ->
n instanceof Label l
&& l.getText() != null
&& l.getText().startsWith("Preview blocked:"))));
repeatHistory(pane, document, changed, mismatched);
click(pane, "Undo");
await(() -> documentText(document).equals(changed));
await(() -> fx(() -> source(pane).getText().equals(changed)));
click(pane, "Render drafts");
await(
() ->
@@ -773,6 +864,7 @@ public final class ProjectionHostChecks {
click(pane, "Apply Repeat binding");
String invalid = replaced.replace("collection=\"visits\"/>", "collection=\"invalid path\"/>");
await(() -> documentText(document).equals(invalid));
await(() -> fx(() -> source(pane).getText().equals(invalid)));
document
.save(document.snapshot().toCompletableFuture().get().revision())
.toCompletableFuture()