From dbccb74b86244eec6c9d9871c1ad41b0e405be7b Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Tue, 15 Sep 2026 15:55:59 -0300 Subject: [PATCH 1/2] feat: support per-row confirmation messages in row actions Close #18 --- FEATURE_ROW_ACTIONS.md | 5 ++ pom.xml | 2 +- .../easygrid/actions/EasyRowAction.java | 40 ++++++++--- .../easygrid/RowActionsDynamicDemo.java | 4 ++ .../easygrid/actions/EasyRowActionTest.java | 68 +++++++++++++++++++ .../addons/easygrid/it/EasyRowActionIT.java | 37 ++++++++-- .../addons/easygrid/it/RmiEasyRowAction.java | 10 +++ 7 files changed, 149 insertions(+), 17 deletions(-) diff --git a/FEATURE_ROW_ACTIONS.md b/FEATURE_ROW_ACTIONS.md index e582e01..c21f2e1 100644 --- a/FEATURE_ROW_ACTIONS.md +++ b/FEATURE_ROW_ACTIONS.md @@ -139,6 +139,11 @@ EasyRowAction withConfirmation(String title, String message); ``` Intercepts button clicks and presents a confirmation dialog before invoking the action handler. The handler is only called if the user confirms. `message` is the confirmation prompt shown to the user; the optional `title` sets the dialog heading. +```java +EasyRowAction withConfirmation(String title, ValueProvider messageProvider); +``` +The same, with the message resolved per row: `messageProvider` is called with the clicked row item while the dialog is being built, so the prompt can name the affected item. + --- #### Styling and theme variants diff --git a/pom.xml b/pom.xml index 65f7f0c..435aebd 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.flowingcode.vaadin.addons easy-grid-addon - 1.0.1-SNAPSHOT + 1.1.0-SNAPSHOT Easy Grid Add-on Easy Grid Add-on for Vaadin Flow https://www.flowingcode.com/en/open-source/ diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowAction.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowAction.java index 4f872e0..d57241a 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowAction.java +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowAction.java @@ -29,8 +29,8 @@ import com.vaadin.flow.component.shared.HasThemeVariant; import com.vaadin.flow.dom.Element; import com.vaadin.flow.function.SerializableConsumer; +import com.vaadin.flow.function.SerializableFunction; import com.vaadin.flow.function.SerializablePredicate; -import com.vaadin.flow.function.SerializableSupplier; import com.vaadin.flow.function.ValueProvider; import java.io.Serializable; import java.lang.reflect.Method; @@ -126,7 +126,7 @@ public void accept(T t) { private SerializablePredicate visibleWhen; private SerializablePredicate enabledWhen; private ValueProvider tooltipProvider; - private SerializableSupplier confirmDialogSupplier; + private SerializableFunction confirmDialogFactory; private transient boolean confirmPending; private void refresh() { @@ -203,15 +203,28 @@ public EasyRowAction withConfirmation(String message) { * @return this action, for method chaining */ public EasyRowAction withConfirmation(String title, String message) { - return withConfirmation(title, message, "Ok", "Cancel"); + return withConfirmation(title, Constant.of(message), "Ok", "Cancel"); } - private EasyRowAction withConfirmation(String title, String message, String confirmText, - String cancelText) { - confirmDialogSupplier = () -> { + /** + * Configures a confirmation dialog with a static title, whose message is computed from the row + * item when the action is clicked. + * + * @param title the dialog title + * @param messageProvider a function that returns the confirmation message for a given row item + * @return this action, for method chaining + */ + public EasyRowAction withConfirmation(String title, + @NonNull ValueProvider messageProvider) { + return withConfirmation(title, messageProvider, "Ok", "Cancel"); + } + + private EasyRowAction withConfirmation(String title, + ValueProvider messageProvider, String confirmText, String cancelText) { + confirmDialogFactory = item -> { var dialog = new ConfirmDialog(); dialog.setHeader(title); - dialog.setText(message); + dialog.setText(messageProvider.apply(item)); dialog.setConfirmText(confirmText); dialog.setCancelable(true); dialog.setCancelText(cancelText); @@ -253,6 +266,15 @@ AbstractIcon getIcon(T item) { return iconProvider != null ? iconProvider.apply(item) : null; } + /** + * Builds the confirmation dialog for the given row item, or returns {@code null} when no + * confirmation is configured. The dialog is created on every call, since its message may be + * derived from the item. + */ + ConfirmDialog getConfirmDialog(T item) { + return confirmDialogFactory != null ? confirmDialogFactory.apply(item) : null; + } + void execute(T item) { // Server-side guard: reject the click if the item no longer satisfies visibleWhen/enabledWhen. // The client-side conditional rendering and ?disabled binding prevent most clicks, but this @@ -261,13 +283,13 @@ void execute(T item) { if (!isVisible(item) || !isEnabled(item)) { return; } - if (confirmDialogSupplier != null) { + if (confirmDialogFactory != null) { // Prevent multiple dialogs from stacking on rapid clicks. if (confirmPending) { return; } confirmPending = true; - ConfirmDialog dialog = confirmDialogSupplier.get(); + ConfirmDialog dialog = getConfirmDialog(item); dialog.addConfirmListener(e -> { if (isVisible(item) && isEnabled(item)) { actionHandler.accept(item); diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/RowActionsDynamicDemo.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/RowActionsDynamicDemo.java index bc9c6f9..c82d3a4 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/easygrid/RowActionsDynamicDemo.java +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/RowActionsDynamicDemo.java @@ -56,6 +56,10 @@ public RowActionsDynamicDemo() { Notification.show("Delete: " + person.getFirstName() + " " + person.getLastName())); deleteAction.addThemeVariants(ButtonVariant.LUMO_ERROR); + // The confirmation message is computed from the row item when the button is clicked. + deleteAction.withConfirmation("Delete person", person -> "Are you sure you want to delete " + + person.getFirstName() + " " + person.getLastName() + "?"); + // Fluent mutators like visibleWhen automatically refresh the grid. var restrictCheckbox = new Checkbox("Show edit only for active persons"); restrictCheckbox.addValueChangeListener(e -> { diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowActionTest.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowActionTest.java index 8e6ea45..088e573 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowActionTest.java +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowActionTest.java @@ -28,10 +28,12 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.confirmdialog.ConfirmDialog; import com.vaadin.flow.component.grid.Grid; import com.vaadin.flow.component.icon.Icon; import com.vaadin.flow.function.SerializableConsumer; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.Test; @@ -219,6 +221,72 @@ public void tooltipOverridesManualTitle() { templateFor(action)); } + // --- confirmation dialog contents --- + // The dialog is built at click time, so its title and message are read back from the + // ConfirmDialog element properties that setHeader/setText write. + + private static String headerOf(ConfirmDialog dialog) { + return dialog.getElement().getProperty("header"); + } + + private static String messageOf(ConfirmDialog dialog) { + return dialog.getElement().getProperty("message"); + } + + @Test + public void withConfirmation_noConfirmation_hasNoDialog() { + var action = new EasyRowAction(null, Constant.of("X"), null, item -> {}); + assertNull(action.getConfirmDialog(7)); + } + + @Test + public void withConfirmation_message_setsMessageAndNoHeader() { + var action = new EasyRowAction(null, Constant.of("X"), null, item -> {}); + action.withConfirmation("Proceed?"); + ConfirmDialog dialog = action.getConfirmDialog(7); + assertNull(headerOf(dialog)); + assertEquals("Proceed?", messageOf(dialog)); + } + + @Test + public void withConfirmation_titleAndMessage_setsHeaderAndMessage() { + var action = new EasyRowAction(null, Constant.of("X"), null, item -> {}); + action.withConfirmation("Confirm", "Proceed?"); + ConfirmDialog dialog = action.getConfirmDialog(7); + assertEquals("Confirm", headerOf(dialog)); + assertEquals("Proceed?", messageOf(dialog)); + } + + @Test + public void withConfirmation_nullTitleAndMessageProvider_derivesMessageAndHasNoHeader() { + var action = new EasyRowAction(null, Constant.of("X"), null, item -> {}); + action.withConfirmation(null, item -> "Delete item " + item + "?"); + assertNull(headerOf(action.getConfirmDialog(7))); + assertEquals("Delete item 7?", messageOf(action.getConfirmDialog(7))); + assertEquals("Delete item 8?", messageOf(action.getConfirmDialog(8))); + } + + @Test + public void withConfirmation_titleAndMessageProvider_derivesMessageFromItem() { + var action = new EasyRowAction(null, Constant.of("X"), null, item -> {}); + action.withConfirmation("Confirm", item -> "Delete item " + item + "?"); + assertEquals("Confirm", headerOf(action.getConfirmDialog(7))); + assertEquals("Delete item 7?", messageOf(action.getConfirmDialog(7))); + assertEquals("Confirm", headerOf(action.getConfirmDialog(8))); + assertEquals("Delete item 8?", messageOf(action.getConfirmDialog(8))); + } + + @Test + public void withConfirmation_providerIsEvaluatedPerDialog() { + var count = new AtomicInteger(); + var action = new EasyRowAction(null, Constant.of("X"), null, item -> {}); + action.withConfirmation("Confirm", item -> "Message " + count.incrementAndGet()); + // The provider is not consulted while configuring the action, only when a dialog is built. + assertEquals(0, count.get()); + assertEquals("Message 1", messageOf(action.getConfirmDialog(7))); + assertEquals("Message 2", messageOf(action.getConfirmDialog(7))); + } + // --- execute: server-side enabledWhen guard --- @Test diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/EasyRowActionIT.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/EasyRowActionIT.java index 12673d2..ac379b2 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/EasyRowActionIT.java +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/EasyRowActionIT.java @@ -34,7 +34,6 @@ import com.vaadin.flow.component.icon.VaadinIcon; import com.vaadin.testbench.ElementQuery; import com.vaadin.testbench.TestBenchElement; -import java.time.Duration; import java.util.List; import lombok.experimental.ExtensionMethod; import org.junit.Before; @@ -42,8 +41,6 @@ import org.openqa.selenium.By; import org.openqa.selenium.Keys; import org.openqa.selenium.StaleElementReferenceException; -import org.openqa.selenium.support.ui.ExpectedConditions; -import org.openqa.selenium.support.ui.WebDriverWait; class ElementQueryExtension { public static T waitForSingle(ElementQuery q) { @@ -193,6 +190,12 @@ public void testEnabledWhen() { assertEquals(Integer.valueOf(2), $server.getClickedValue()); // unchanged } + // Waits until the confirmation dialog is gone, so that a subsequent click does not observe the + // dialog that is still closing. + private void waitForNoConfirmDialog() { + waitUntil(d -> $(ConfirmDialogElement.class).all().isEmpty()); + } + @Test public void testConfirmation() { var action = $server.addRowAction(VaadinIcon.VAADIN_H, $server.action(1)); @@ -217,6 +220,29 @@ public void testConfirmation() { assertEquals(1, $(ConfirmDialogElement.class).all().size()); } + @Test + public void testDynamicConfirmation() { + var action = $server.addRowAction(VaadinIcon.VAADIN_H, $server.action(1)); + action.withConfirmation("Confirm", x -> "Delete item " + x + "?"); + + // the dialog message is computed from the clicked row (row 0 = item 1) + grid.getCell(0, 1).$("vaadin-button").single().click(); + var dialog = $(ConfirmDialogElement.class).waitForSingle(); + assertEquals("Confirm", dialog.getHeaderText()); + assertEquals("Delete item 1?", dialog.getMessageText()); + dialog.getCancelButton().click(); + + waitForNoConfirmDialog(); + + // a different row yields a different message (row 1 = item 2) + grid.getCell(1, 1).$("vaadin-button").single().click(); + dialog = $(ConfirmDialogElement.class).waitForSingle(); + assertEquals("Confirm", dialog.getHeaderText()); + assertEquals("Delete item 2?", dialog.getMessageText()); + dialog.getConfirmButton().click(); + assertEquals(Integer.valueOf(2), $server.getClickedValue()); + } + @Test public void testContextMenu() { $server.setRowActionsStyle(RowActionsStyle.CONTEXT_MENU); @@ -278,10 +304,7 @@ public void testContextMenuConfirmation() throws InterruptedException { dialog.getCancelButton().click(); assertNull($server.getClickedValue()); - new WebDriverWait(getDriver(), - Duration.ofSeconds(1)) - .until(ExpectedConditions - .numberOfElementsToBe(By.tagName("vaadin-confirm-dialog-overlay"), 0)); + waitForNoConfirmDialog(); // selecting it again and confirming fires the handler (row 0 = item 1) grid.getCell(0, 0).contextClick(); diff --git a/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/RmiEasyRowAction.java b/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/RmiEasyRowAction.java index c9df9c9..1017610 100644 --- a/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/RmiEasyRowAction.java +++ b/src/test/java/com/flowingcode/vaadin/addons/easygrid/it/RmiEasyRowAction.java @@ -23,6 +23,7 @@ import com.flowingcode.vaadin.testbench.rpc.RmiRemote; import com.vaadin.flow.component.button.ButtonVariant; import com.vaadin.flow.function.SerializablePredicate; +import com.vaadin.flow.function.ValueProvider; /** * RMI interface for EasyRowAction proxying in integration tests. @@ -38,6 +39,8 @@ public interface RmiEasyRowAction extends RmiRemote { RmiEasyRowAction withConfirmation(String title, String message); + RmiEasyRowAction withConfirmation(String title, ValueProvider messageProvider); + RmiEasyRowAction addThemeVariants(ButtonVariant variant); void remove(); @@ -62,6 +65,13 @@ public RmiEasyRowAction withConfirmation(String title, String message) { return this; } + @Override + public RmiEasyRowAction withConfirmation(String title, + ValueProvider messageProvider) { + action.withConfirmation(title, messageProvider); + return this; + } + @Override public RmiEasyRowAction addThemeVariants(ButtonVariant variant) { action.addThemeVariants(variant); From b3f0bfd63883b36f94d15c1040d0767442c6c411 Mon Sep 17 00:00:00 2001 From: Javier Godoy <11554739+javier-godoy@users.noreply.github.com> Date: Tue, 15 Sep 2026 16:06:21 -0300 Subject: [PATCH 2/2] WIP: Update src/main/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowAction.java Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../vaadin/addons/easygrid/actions/EasyRowAction.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowAction.java b/src/main/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowAction.java index d57241a..d581909 100644 --- a/src/main/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowAction.java +++ b/src/main/java/com/flowingcode/vaadin/addons/easygrid/actions/EasyRowAction.java @@ -289,7 +289,13 @@ void execute(T item) { return; } confirmPending = true; - ConfirmDialog dialog = getConfirmDialog(item); + ConfirmDialog dialog; + try { + dialog = getConfirmDialog(item); + } catch (RuntimeException ex) { + confirmPending = false; + throw ex; + } dialog.addConfirmListener(e -> { if (isVisible(item) && isEnabled(item)) { actionHandler.accept(item);