diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardDragDropPage.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardDragDropPage.java new file mode 100644 index 00000000000..71fd88d187d --- /dev/null +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardDragDropPage.java @@ -0,0 +1,72 @@ +/** + * Copyright 2000-2024 Vaadin Ltd. + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See {@literal } for the full + * license. + */ +package com.vaadin.flow.component.dashboard.tests; + +import com.vaadin.flow.component.dashboard.Dashboard; +import com.vaadin.flow.component.dashboard.DashboardSection; +import com.vaadin.flow.component.dashboard.DashboardWidget; +import com.vaadin.flow.component.html.Div; +import com.vaadin.flow.component.html.NativeButton; +import com.vaadin.flow.router.Route; + +/** + * @author Vaadin Ltd + */ +@Route("vaadin-dashboard/drag-drop") +public class DashboardDragDropPage extends Div { + + public DashboardDragDropPage() { + Dashboard dashboard = new Dashboard(); + dashboard.setEditable(true); + dashboard.setMinimumRowHeight("100px"); + dashboard.setMaximumColumnWidth("400px"); + + DashboardWidget widget1 = new DashboardWidget(); + widget1.setTitle("Widget 1"); + + DashboardWidget widget2 = new DashboardWidget(); + widget2.setTitle("Widget 2"); + + dashboard.add(widget1, widget2); + + DashboardWidget widget1InSection1 = new DashboardWidget(); + widget1InSection1.setTitle("Widget 1 in Section 1"); + + DashboardWidget widget2InSection1 = new DashboardWidget(); + widget2InSection1.setTitle("Widget 2 in Section 1"); + + DashboardSection section1 = new DashboardSection("Section 1"); + section1.add(widget1InSection1, widget2InSection1); + + dashboard.addSection(section1); + + DashboardWidget widgetInSection2 = new DashboardWidget(); + widgetInSection2.setTitle("Widget in Section 2"); + + DashboardSection section2 = new DashboardSection("Section 2"); + section2.add(widgetInSection2); + + dashboard.addSection(section2); + + NativeButton toggleAttached = new NativeButton("Toggle attached", e -> { + if (dashboard.getParent().isPresent()) { + dashboard.removeFromParent(); + } else { + add(dashboard); + } + }); + toggleAttached.setId("toggle-attached"); + + NativeButton toggleEditable = new NativeButton("Toggle editable", + e -> dashboard.setEditable(!dashboard.isEditable())); + toggleEditable.setId("toggle-editable"); + + add(toggleAttached, toggleEditable, dashboard); + } +} diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardSectionPage.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardSectionPage.java index 79f882d0b68..5833a39dcd2 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardSectionPage.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardSectionPage.java @@ -21,7 +21,7 @@ /** * @author Vaadin Ltd */ -@Route("vaadin-dashboard-section") +@Route("vaadin-dashboard/section") public class DashboardSectionPage extends Div { public DashboardSectionPage() { diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetPage.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetPage.java index 27eb5c834e5..749963a6e5f 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetPage.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetPage.java @@ -20,7 +20,7 @@ /** * @author Vaadin Ltd */ -@Route("vaadin-dashboard-widget") +@Route("vaadin-dashboard/widget") public class DashboardWidgetPage extends Div { public DashboardWidgetPage() { diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardDragDropIT.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardDragDropIT.java new file mode 100644 index 00000000000..3812195cb5a --- /dev/null +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardDragDropIT.java @@ -0,0 +1,111 @@ +/** + * Copyright 2000-2024 Vaadin Ltd. + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See {@literal } for the full + * license. + */ +package com.vaadin.flow.component.dashboard.tests; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.openqa.selenium.interactions.Actions; + +import com.vaadin.flow.component.dashboard.testbench.DashboardElement; +import com.vaadin.flow.testutil.TestPath; +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.tests.AbstractComponentIT; + +/** + * @author Vaadin Ltd + */ +@TestPath("vaadin-dashboard/drag-drop") +public class DashboardDragDropIT extends AbstractComponentIT { + + private DashboardElement dashboardElement; + + @Before + public void init() { + open(); + dashboardElement = $(DashboardElement.class).waitForFirst(); + } + + @Test + public void reorderWidgetOnClientSide_itemsAreReorderedCorrectly() { + var draggedWidget = dashboardElement.getWidgets().get(0); + var targetWidget = dashboardElement.getWidgets().get(1); + dragDropElement(draggedWidget, targetWidget); + Assert.assertEquals(draggedWidget.getTitle(), + dashboardElement.getWidgets().get(1).getTitle()); + } + + @Test + public void reorderSectionOnClientSide_itemsAreReorderedCorrectly() { + var draggedSection = dashboardElement.getSections().get(1); + var targetWidget = dashboardElement.getWidgets().get(0); + dragDropElement(draggedSection, targetWidget); + Assert.assertEquals(draggedSection.getTitle(), + dashboardElement.getSections().get(0).getTitle()); + } + + @Test + public void reorderWidgetInSectionOnClientSide_itemsAreReorderedCorrectly() { + var firstSection = dashboardElement.getSections().get(0); + var draggedWidget = firstSection.getWidgets().get(0); + var targetWidget = firstSection.getWidgets().get(1); + dragDropElement(draggedWidget, targetWidget); + firstSection = dashboardElement.getSections().get(0); + Assert.assertEquals(draggedWidget.getTitle(), + firstSection.getWidgets().get(1).getTitle()); + } + + @Test + public void detachReattach_reorderWidgetOnClientSide_itemsAreReorderedCorrectly() { + clickElementWithJs("toggle-attached"); + clickElementWithJs("toggle-attached"); + dashboardElement = $(DashboardElement.class).waitForFirst(); + reorderWidgetOnClientSide_itemsAreReorderedCorrectly(); + } + + @Test + public void setDashboardNotEditable_widgetCannotBeDragged() { + var widget = dashboardElement.getWidgets().get(0); + Assert.assertTrue(isHeaderActionsVisible(widget)); + clickElementWithJs("toggle-editable"); + Assert.assertFalse(isHeaderActionsVisible(widget)); + } + + @Test + public void setDashboardEditable_widgetCanBeDragged() { + clickElementWithJs("toggle-editable"); + clickElementWithJs("toggle-editable"); + Assert.assertTrue( + isHeaderActionsVisible(dashboardElement.getWidgets().get(0))); + } + + private void dragDropElement(TestBenchElement draggedElement, + TestBenchElement targetElement) { + var dragHandle = getDragHandle(draggedElement); + + var yOffset = draggedElement.getLocation().getY() < targetElement + .getLocation().getY() ? 10 : -10; + var xOffset = draggedElement.getLocation().getX() < targetElement + .getLocation().getX() ? 10 : -10; + + new Actions(driver).clickAndHold(dragHandle) + .moveToElement(targetElement, xOffset, yOffset) + .release(targetElement).build().perform(); + } + + private static boolean isHeaderActionsVisible(TestBenchElement element) { + TestBenchElement headerActions = element.$("*").withId("header-actions") + .first(); + return !"none".equals(headerActions.getCssValue("display")); + } + + private static TestBenchElement getDragHandle(TestBenchElement element) { + return element.$("*").withClassName("drag-handle").first(); + } +} diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardSectionIT.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardSectionIT.java index 31e275b8561..171d2eec48c 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardSectionIT.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardSectionIT.java @@ -24,7 +24,7 @@ /** * @author Vaadin Ltd */ -@TestPath("vaadin-dashboard-section") +@TestPath("vaadin-dashboard/section") public class DashboardSectionIT extends AbstractComponentIT { private DashboardElement dashboardElement; diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetIT.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetIT.java index 02d31dee991..e3455bd93ac 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetIT.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetIT.java @@ -22,7 +22,7 @@ /** * @author Vaadin Ltd */ -@TestPath("vaadin-dashboard-widget") +@TestPath("vaadin-dashboard/widget") public class DashboardWidgetIT extends AbstractComponentIT { private DashboardElement dashboardElement; diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/Dashboard.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/Dashboard.java index 9759822f802..b15ae3bff47 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/Dashboard.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/Dashboard.java @@ -9,11 +9,12 @@ package com.vaadin.flow.component.dashboard; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -21,10 +22,15 @@ import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.ComponentEventListener; import com.vaadin.flow.component.Tag; import com.vaadin.flow.component.dependency.JsModule; import com.vaadin.flow.component.dependency.NpmPackage; import com.vaadin.flow.dom.Element; +import com.vaadin.flow.shared.Registration; + +import elemental.json.JsonArray; +import elemental.json.JsonObject; /** * @author Vaadin Ltd @@ -48,6 +54,7 @@ public class Dashboard extends Component implements HasWidgets { */ public Dashboard() { childDetachHandler = getChildDetachHandler(); + addItemReorderEndListener(this::onItemReorderEnd); } /** @@ -275,6 +282,49 @@ public void setGap(String gap) { getStyle().set("--vaadin-dashboard-gap", gap); } + /** + * Sets the option to make the dashboard editable. + * + * @param editable + * whether to set the dashboard editable + */ + public void setEditable(boolean editable) { + getElement().setProperty("editable", editable); + } + + /** + * Returns whether the dashboard is editable. + * + * @return whether to set the dashboard editable + */ + public boolean isEditable() { + return getElement().getProperty("editable", false); + } + + /** + * Adds an item reorder start listener to this dashboard. + * + * @param listener + * the listener to add, not null + * @return a handle that can be used for removing the listener + */ + public Registration addItemReorderStartListener( + ComponentEventListener listener) { + return addListener(DashboardItemReorderStartEvent.class, listener); + } + + /** + * Adds an item reorder end listener to this dashboard. + * + * @param listener + * the listener to add, not null + * @return a handle that can be used for removing the listener + */ + public Registration addItemReorderEndListener( + ComponentEventListener listener) { + return addListener(DashboardItemReorderEndEvent.class, listener); + } + @Override public Stream getChildren() { return childrenComponents.stream(); @@ -285,6 +335,7 @@ protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); getElement().executeJs( "Vaadin.FlowComponentHost.patchVirtualContainer(this);"); + customizeItemReorderEndEvent(); doUpdateClient(); } @@ -320,8 +371,9 @@ private void updateClientItems() { .map(widget -> getWidgetRepresentation(widget, itemIndex.getAndIncrement())) .collect(Collectors.joining(",")); - itemRepresentation = "{ component: $%d, items: [ %s ] }" - .formatted(sectionIndex, sectionWidgetsRepresentation); + itemRepresentation = "{ component: $%d, items: [ %s ], nodeid: %d }" + .formatted(sectionIndex, sectionWidgetsRepresentation, + section.getElement().getNode().getId()); } else { itemRepresentation = getWidgetRepresentation( (DashboardWidget) component, @@ -337,8 +389,9 @@ private void updateClientItems() { private static String getWidgetRepresentation(DashboardWidget widget, int itemIndex) { - return "{ component: $%d, colspan: %d, rowspan: %d }" - .formatted(itemIndex, widget.getColspan(), widget.getRowspan()); + return "{ component: $%d, colspan: %d, rowspan: %d, nodeid: %d }" + .formatted(itemIndex, widget.getColspan(), widget.getRowspan(), + widget.getElement().getNode().getId()); } private void doRemoveAll() { @@ -377,17 +430,82 @@ private void doRemoveSection(DashboardSection section) { } private DashboardChildDetachHandler getChildDetachHandler() { - return new DashboardChildDetachHandler() { + return new DashboardChildDetachHandler(this) { @Override void removeChild(Component child) { childrenComponents.remove(child); updateClient(); } + }; + } - @Override - Collection getDirectChildren() { - return Dashboard.this.getChildren().toList(); + private void onItemReorderEnd( + DashboardItemReorderEndEvent dashboardItemReorderEndEvent) { + JsonArray orderedItemsFromClient = dashboardItemReorderEndEvent + .getItems(); + reorderItems(orderedItemsFromClient); + updateClient(); + } + + private void reorderItems(JsonArray orderedItemsFromClient) { + // Keep references to the root level children before clearing them + Map nodeIdToComponent = childrenComponents.stream() + .collect(Collectors.toMap( + component -> component.getElement().getNode().getId(), + Function.identity())); + // Remove all children and add them back using the node IDs from client + // items + childrenComponents.clear(); + for (int rootLevelItemIdx = 0; rootLevelItemIdx < orderedItemsFromClient + .length(); rootLevelItemIdx++) { + JsonObject rootLevelItemFromClient = orderedItemsFromClient + .getObject(rootLevelItemIdx); + int rootLevelItemNodeId = (int) rootLevelItemFromClient + .getNumber("nodeid"); + Component componentMatch = nodeIdToComponent + .get(rootLevelItemNodeId); + childrenComponents.add(componentMatch); + // Reorder the widgets in sections separately + if (componentMatch instanceof DashboardSection sectionMatch) { + reorderSectionWidgets(sectionMatch, rootLevelItemFromClient); } - }; + } + } + + private void reorderSectionWidgets(DashboardSection section, + JsonObject rootLevelItem) { + // Keep references to the widgets before clearing them + Map nodeIdToWidget = section.getWidgets() + .stream() + .collect(Collectors.toMap( + widget -> widget.getElement().getNode().getId(), + Function.identity())); + // Remove all widgets and add them back using the node IDs from client + // items + section.removeAll(); + JsonArray sectionWidgetsFromClient = rootLevelItem.getArray("items"); + for (int sectionWidgetIdx = 0; sectionWidgetIdx < sectionWidgetsFromClient + .length(); sectionWidgetIdx++) { + int sectionItemNodeId = (int) sectionWidgetsFromClient + .getObject(sectionWidgetIdx).getNumber("nodeid"); + section.add(nodeIdToWidget.get(sectionItemNodeId)); + } + } + + private void customizeItemReorderEndEvent() { + getElement().executeJs( + """ + this.addEventListener('dashboard-item-reorder-end', (e) => { + function mapItems(items) { + return items.map(({nodeid, items}) => ({ + nodeid, + ...(items && { items: mapItems(items) }) + })); + } + const flowReorderEvent = new CustomEvent('dashboard-item-reorder-end-flow', { + detail: { items: mapItems(this.items) } + }); + this.dispatchEvent(flowReorderEvent); + });"""); } } diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardChildDetachHandler.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardChildDetachHandler.java index 64cda277be1..b8ff68b842f 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardChildDetachHandler.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardChildDetachHandler.java @@ -8,7 +8,6 @@ */ package com.vaadin.flow.component.dashboard; -import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.Objects; @@ -22,28 +21,31 @@ public abstract class DashboardChildDetachHandler implements ElementDetachListener { + private final Component component; + private final Map childDetachListenerMap = new HashMap<>(); + DashboardChildDetachHandler(Component component) { + this.component = component; + } + @Override public void onDetach(ElementDetachEvent e) { var detachedElement = e.getSource(); - getDirectChildren().stream() - .filter(childComponent -> Objects.equals(detachedElement, - childComponent.getElement())) - .findAny().ifPresent(detachedChild -> { - // The child was removed from the component - - // Remove the registration for the child detach listener - childDetachListenerMap.get(detachedChild.getElement()) - .remove(); - childDetachListenerMap.remove(detachedChild.getElement()); - - removeChild(detachedChild); - }); + var childDetachedFromContainer = component.getElement().getChildren() + .noneMatch(containerChild -> Objects.equals(detachedElement, + containerChild)); + if (childDetachedFromContainer) { + // The child was removed from the component + // Remove the registration for the child detach listener + childDetachListenerMap.get(detachedElement).remove(); + childDetachListenerMap.remove(detachedElement); + detachedElement.getComponent().ifPresent(this::removeChild); + } } void refreshListeners() { - getDirectChildren().forEach(child -> { + component.getChildren().forEach(child -> { Element childElement = child.getElement(); if (!childDetachListenerMap.containsKey(childElement)) { childDetachListenerMap.put(childElement, @@ -53,6 +55,4 @@ void refreshListeners() { } abstract void removeChild(Component child); - - abstract Collection getDirectChildren(); } diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardItemReorderEndEvent.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardItemReorderEndEvent.java new file mode 100644 index 00000000000..22ff4dbf79e --- /dev/null +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardItemReorderEndEvent.java @@ -0,0 +1,52 @@ +/** + * Copyright 2000-2024 Vaadin Ltd. + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See {@literal } for the full + * license. + */ +package com.vaadin.flow.component.dashboard; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.DomEvent; +import com.vaadin.flow.component.EventData; + +import elemental.json.JsonArray; + +/** + * Widget or section reorder end event of {@link Dashboard}. + * + * @author Vaadin Ltd. + * @see Dashboard#addItemReorderEndListener(ComponentEventListener) + */ +@DomEvent("dashboard-item-reorder-end-flow") +public class DashboardItemReorderEndEvent extends ComponentEvent { + + private final JsonArray items; + + /** + * Creates a dashboard item reorder end event. + * + * @param source + * Dashboard that contains the item that was dragged + * @param fromClient + * true if the event originated from the client + * side, false otherwise + */ + public DashboardItemReorderEndEvent(Dashboard source, boolean fromClient, + @EventData("event.detail.items") JsonArray items) { + super(source, fromClient); + this.items = items; + } + + /** + * Returns the ordered items from the client side + * + * @return items the ordered items as a {@link JsonArray} + */ + public JsonArray getItems() { + return items; + } +} diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardItemReorderStartEvent.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardItemReorderStartEvent.java new file mode 100644 index 00000000000..d2910539cfb --- /dev/null +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardItemReorderStartEvent.java @@ -0,0 +1,37 @@ +/** + * Copyright 2000-2024 Vaadin Ltd. + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See {@literal } for the full + * license. + */ +package com.vaadin.flow.component.dashboard; + +import com.vaadin.flow.component.ComponentEvent; +import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.DomEvent; + +/** + * Widget or section reorder start event of {@link Dashboard}. + * + * @author Vaadin Ltd. + * @see Dashboard#addItemReorderStartListener(ComponentEventListener) + */ +@DomEvent("dashboard-item-reorder-start") +public class DashboardItemReorderStartEvent extends ComponentEvent { + + /** + * Creates a dashboard item reorder start event. + * + * @param source + * Dashboard that contains the item that was dragged + * @param fromClient + * true if the event originated from the client + * side, false otherwise + */ + public DashboardItemReorderStartEvent(Dashboard source, + boolean fromClient) { + super(source, fromClient); + } +} diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardSection.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardSection.java index f899904b324..a3064abfe1b 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardSection.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardSection.java @@ -9,7 +9,6 @@ package com.vaadin.flow.component.dashboard; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -177,17 +176,12 @@ void updateClient() { } private DashboardChildDetachHandler getChildDetachHandler() { - return new DashboardChildDetachHandler() { + return new DashboardChildDetachHandler(this) { @Override void removeChild(Component child) { widgets.remove(child); updateClient(); } - - @Override - Collection getDirectChildren() { - return DashboardSection.this.getChildren().toList(); - } }; } } diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardDragDropTest.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardDragDropTest.java new file mode 100644 index 00000000000..a1924235508 --- /dev/null +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardDragDropTest.java @@ -0,0 +1,148 @@ +/** + * Copyright 2000-2024 Vaadin Ltd. + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See {@literal } for the full + * license. + */ +package com.vaadin.flow.component.dashboard.tests; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.vaadin.flow.component.ComponentUtil; +import com.vaadin.flow.component.dashboard.Dashboard; +import com.vaadin.flow.component.dashboard.DashboardItemReorderEndEvent; +import com.vaadin.flow.component.dashboard.DashboardSection; +import com.vaadin.flow.component.dashboard.DashboardWidget; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonObject; + +public class DashboardDragDropTest extends DashboardTestBase { + private Dashboard dashboard; + + private JsonArray itemsArray; + + @Before + @Override + public void setup() { + super.setup(); + dashboard = new Dashboard(); + dashboard.add(new DashboardWidget(), new DashboardWidget()); + DashboardSection section = dashboard.addSection(); + section.add(new DashboardWidget(), new DashboardWidget()); + getUi().add(dashboard); + fakeClientCommunication(); + itemsArray = getItemsArray(dashboard.getChildren().toList()); + } + + @Test + public void reorderWidget_orderIsUpdated() { + assertRootLevelItemReorder(0, 1); + } + + @Test + public void reorderSection_orderIsUpdated() { + assertRootLevelItemReorder(2, 1); + } + + @Test + public void reorderWidgetInSection_orderIsUpdated() { + assertSectionWidgetReorder(2, 0, 1); + } + + private void fireItemReorderEndEvent() { + ComponentUtil.fireEvent(dashboard, + new DashboardItemReorderEndEvent(dashboard, false, itemsArray)); + } + + private List getSectionWidgetNodeIds(int sectionIndex) { + DashboardSection section = (DashboardSection) dashboard.getChildren() + .toList().get(sectionIndex); + return section.getWidgets().stream() + .map(component -> component.getElement().getNode().getId()) + .collect(Collectors.toCollection(ArrayList::new)); + } + + private List getRootLevelNodeIds() { + return dashboard.getChildren() + .map(component -> component.getElement().getNode().getId()) + .collect(Collectors.toCollection(ArrayList::new)); + } + + private List getExpectedSectionWidgetNodeIds(int sectionIndex, + int initialIndex, int finalIndex) { + List expectedSectionWidgetNodeIds = getSectionWidgetNodeIds( + sectionIndex); + int nodeId = expectedSectionWidgetNodeIds.get(initialIndex); + expectedSectionWidgetNodeIds.remove((Object) nodeId); + expectedSectionWidgetNodeIds.add(finalIndex, nodeId); + return expectedSectionWidgetNodeIds; + } + + private List getExpectedRootLevelItemNodeIds(int initialIndex, + int finalIndex) { + List expectedRootLevelNodeIds = getRootLevelNodeIds(); + int nodeId = expectedRootLevelNodeIds.get(initialIndex); + expectedRootLevelNodeIds.remove((Object) nodeId); + expectedRootLevelNodeIds.add(finalIndex, nodeId); + return expectedRootLevelNodeIds; + } + + private void reorderSectionWidget(int sectionIndex, int initialIndex, + int finalIndex) { + JsonObject sectionItem = itemsArray.get(sectionIndex); + JsonArray sectionItems = sectionItem.getArray("items"); + sectionItem.put("items", + reorderItemInJsonArray(initialIndex, finalIndex, sectionItems)); + } + + private void reorderRootLevelItem(int initialIndex, int finalIndex) { + itemsArray = reorderItemInJsonArray(initialIndex, finalIndex, + itemsArray); + } + + private void assertSectionWidgetReorder(int sectionIndex, int initialIndex, + int finalIndex) { + reorderSectionWidget(sectionIndex, initialIndex, finalIndex); + List expectedSectionWidgetNodeIds = getExpectedSectionWidgetNodeIds( + sectionIndex, initialIndex, finalIndex); + fireItemReorderEndEvent(); + Assert.assertEquals(expectedSectionWidgetNodeIds, + getSectionWidgetNodeIds(sectionIndex)); + } + + private void assertRootLevelItemReorder(int initialIndex, int finalIndex) { + + reorderRootLevelItem(initialIndex, finalIndex); + List expectedRootLevelNodeIds = getExpectedRootLevelItemNodeIds( + initialIndex, finalIndex); + fireItemReorderEndEvent(); + Assert.assertEquals(expectedRootLevelNodeIds, getRootLevelNodeIds()); + } + + private static JsonArray reorderItemInJsonArray(int initialIndex, + int finalIndex, JsonArray initialArray) { + JsonObject itemToMove = initialArray.get(initialIndex); + initialArray.remove(initialIndex); + JsonArray newArray = Json.createArray(); + for (int i = 0; i < finalIndex; i++) { + JsonObject currentItem = initialArray.get(i); + newArray.set(i, currentItem); + } + newArray.set(finalIndex, itemToMove); + for (int i = finalIndex; i < initialArray.length(); i++) { + JsonObject currentItem = initialArray.get(i); + newArray.set(i + 1, currentItem); + } + return newArray; + } +} diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardTest.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardTest.java index c48d59c39eb..babd21ee882 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardTest.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardTest.java @@ -8,44 +8,27 @@ */ package com.vaadin.flow.component.dashboard.tests; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.mockito.Mockito; -import com.vaadin.flow.component.Component; -import com.vaadin.flow.component.UI; import com.vaadin.flow.component.dashboard.Dashboard; import com.vaadin.flow.component.dashboard.DashboardSection; import com.vaadin.flow.component.dashboard.DashboardWidget; import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.server.VaadinSession; -public class DashboardTest { - private final UI ui = new UI(); +public class DashboardTest extends DashboardTestBase { private Dashboard dashboard; @Before + @Override public void setup() { - UI.setCurrent(ui); - VaadinSession session = Mockito.mock(VaadinSession.class); - Mockito.when(session.hasLock()).thenReturn(true); - ui.getInternals().setSession(session); + super.setup(); dashboard = new Dashboard(); - ui.add(dashboard); + getUi().add(dashboard); fakeClientCommunication(); } - @After - public void tearDown() { - UI.setCurrent(null); - } - @Test public void addWidget_widgetIsAdded() { DashboardWidget widget1 = new DashboardWidget(); @@ -176,7 +159,7 @@ public void addWidgetsSeparately_removeOneFromParent_widgetIsRemoved() { @Test public void addWidgetFromLayoutToDashboard_widgetIsMoved() { Div parent = new Div(); - ui.add(parent); + getUi().add(parent); DashboardWidget widget = new DashboardWidget(); parent.add(widget); fakeClientCommunication(); @@ -192,7 +175,7 @@ public void addWidgetFromDashboardToLayout_widgetIsMoved() { dashboard.add(widget); fakeClientCommunication(); Div parent = new Div(); - ui.add(parent); + getUi().add(parent); parent.add(widget); fakeClientCommunication(); assertChildComponents(dashboard); @@ -205,7 +188,7 @@ public void addWidgetToAnotherDashboard_widgetIsMoved() { dashboard.add(widget); fakeClientCommunication(); Dashboard newDashboard = new Dashboard(); - ui.add(newDashboard); + getUi().add(newDashboard); newDashboard.add(widget); fakeClientCommunication(); assertChildComponents(dashboard); @@ -545,7 +528,7 @@ public void addWidgetsSeparatelyToSection_removeOneFromParent_widgetIsRemoved() public void addWidgetFromLayoutToSection_widgetIsMoved() { DashboardSection section = dashboard.addSection(); Div parent = new Div(); - ui.add(parent); + getUi().add(parent); DashboardWidget widget = new DashboardWidget(); parent.add(widget); fakeClientCommunication(); @@ -563,7 +546,7 @@ public void addWidgetFromSectionToLayout_widgetIsMoved() { section.add(widget); fakeClientCommunication(); Div parent = new Div(); - ui.add(parent); + getUi().add(parent); parent.add(widget); fakeClientCommunication(); assertSectionWidgets(section); @@ -795,40 +778,41 @@ public void setGapNull_valueIsCorrectlyRetrieved() { Assert.assertNull(dashboard.getGap()); } - private void fakeClientCommunication() { - ui.getInternals().getStateTree().runExecutionsBeforeClientResponse(); - ui.getInternals().getStateTree().collectChanges(ignore -> { - }); - } - - private static void assertChildComponents(Dashboard dashboard, - Component... expectedChildren) { - List expectedWidgets = getExpectedWidgets( - expectedChildren); - Assert.assertEquals(expectedWidgets, dashboard.getWidgets()); - Assert.assertEquals(Arrays.asList(expectedChildren), - dashboard.getChildren().toList()); - } - - private static List getExpectedWidgets( - Component... expectedChildren) { - List expectedWidgets = new ArrayList<>(); - for (Component child : expectedChildren) { - if (child instanceof DashboardSection section) { - expectedWidgets.addAll(section.getWidgets()); - } else if (child instanceof DashboardWidget widget) { - expectedWidgets.add(widget); - } else { - throw new IllegalArgumentException( - "A dashboard can only contain widgets or sections."); - } - } - return expectedWidgets; + @Test + public void dashboardIsNotEditableByDefault() { + Assert.assertFalse(dashboard.isEditable()); } - private static void assertSectionWidgets(DashboardSection section, - DashboardWidget... expectedWidgets) { - Assert.assertEquals(Arrays.asList(expectedWidgets), - section.getWidgets()); + @Test + public void setEditableFalse_valueIsCorrectlyRetrieved() { + dashboard.setEditable(false); + Assert.assertFalse(dashboard.isEditable()); + } + + @Test + public void setEditableTrue_valueIsCorrectlyRetrieved() { + dashboard.setEditable(false); + dashboard.setEditable(true); + Assert.assertTrue(dashboard.isEditable()); + } + + @Test + public void addWidget_detachDashboard_widgetIsRetained() { + DashboardWidget widget = new DashboardWidget(); + dashboard.add(widget); + fakeClientCommunication(); + getUi().remove(dashboard); + fakeClientCommunication(); + assertChildComponents(dashboard, widget); + } + + @Test + public void detachDashboard_addWidget_reattachDashboard_widgetIsAdded() { + getUi().remove(dashboard); + fakeClientCommunication(); + DashboardWidget widget = new DashboardWidget(); + dashboard.add(widget); + fakeClientCommunication(); + assertChildComponents(dashboard, widget); } } diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardTestBase.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardTestBase.java new file mode 100644 index 00000000000..cc72fad938d --- /dev/null +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardTestBase.java @@ -0,0 +1,111 @@ +/** + * Copyright 2000-2024 Vaadin Ltd. + * + * This program is available under Vaadin Commercial License and Service Terms. + * + * See {@literal } for the full + * license. + */ +package com.vaadin.flow.component.dashboard.tests; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.mockito.Mockito; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.component.UI; +import com.vaadin.flow.component.dashboard.Dashboard; +import com.vaadin.flow.component.dashboard.DashboardSection; +import com.vaadin.flow.component.dashboard.DashboardWidget; +import com.vaadin.flow.server.VaadinSession; + +import elemental.json.Json; +import elemental.json.JsonArray; +import elemental.json.JsonObject; + +public class DashboardTestBase { + + private final UI ui = new UI(); + + @Before + public void setup() { + UI.setCurrent(ui); + VaadinSession session = Mockito.mock(VaadinSession.class); + Mockito.when(session.hasLock()).thenReturn(true); + ui.getInternals().setSession(session); + fakeClientCommunication(); + } + + @After + public void tearDown() { + UI.setCurrent(null); + } + + protected UI getUi() { + return ui; + } + + protected void fakeClientCommunication() { + ui.getInternals().getStateTree().runExecutionsBeforeClientResponse(); + ui.getInternals().getStateTree().collectChanges(ignore -> { + }); + } + + protected static JsonArray getItemsArray( + List rootLevelComponents) { + JsonArray itemsArray = Json.createArray(); + rootLevelComponents.forEach(child -> { + JsonObject rootLevelItem = Json.createObject(); + rootLevelItem.put("nodeid", child.getElement().getNode().getId()); + if (child instanceof DashboardSection section) { + JsonArray sectionItemsArray = Json.createArray(); + section.getWidgets().forEach(widget -> { + JsonObject sectionItem = Json.createObject(); + sectionItem.put("nodeid", + widget.getElement().getNode().getId()); + sectionItemsArray.set(sectionItemsArray.length(), + sectionItem); + }); + rootLevelItem.put("items", sectionItemsArray); + } + itemsArray.set(itemsArray.length(), rootLevelItem); + }); + return itemsArray; + } + + protected static void assertChildComponents(Dashboard dashboard, + Component... expectedChildren) { + List expectedWidgets = getExpectedWidgets( + expectedChildren); + Assert.assertEquals(expectedWidgets, dashboard.getWidgets()); + Assert.assertEquals(Arrays.asList(expectedChildren), + dashboard.getChildren().toList()); + } + + protected static List getExpectedWidgets( + Component... expectedChildren) { + List expectedWidgets = new ArrayList<>(); + for (Component child : expectedChildren) { + if (child instanceof DashboardSection section) { + expectedWidgets.addAll(section.getWidgets()); + } else if (child instanceof DashboardWidget widget) { + expectedWidgets.add(widget); + } else { + throw new IllegalArgumentException( + "A dashboard can only contain widgets or sections."); + } + } + return expectedWidgets; + } + + protected static void assertSectionWidgets(DashboardSection section, + DashboardWidget... expectedWidgets) { + Assert.assertEquals(Arrays.asList(expectedWidgets), + section.getWidgets()); + } +} diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetTest.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetTest.java index d5be4ef5180..91924e1f74d 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetTest.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardWidgetTest.java @@ -8,39 +8,41 @@ */ package com.vaadin.flow.component.dashboard.tests; -import org.junit.After; import org.junit.Assert; -import org.junit.Before; import org.junit.Test; -import org.mockito.Mockito; -import com.vaadin.flow.component.UI; import com.vaadin.flow.component.dashboard.DashboardWidget; import com.vaadin.flow.component.html.Div; import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.server.VaadinSession; -public class DashboardWidgetTest { +public class DashboardWidgetTest extends DashboardTestBase { - private final UI ui = new UI(); + @Test + public void assertDefaultTitle() { + DashboardWidget widget = new DashboardWidget(); + Assert.assertNull(widget.getTitle()); + } - @Before - public void setup() { - UI.setCurrent(ui); - VaadinSession session = Mockito.mock(VaadinSession.class); - Mockito.when(session.hasLock()).thenReturn(true); - ui.getInternals().setSession(session); + @Test + public void setTitle_returnsCorrectTitle() { + String valueToSet = "New title"; + DashboardWidget widget = new DashboardWidget(); + widget.setTitle(valueToSet); + Assert.assertEquals(valueToSet, widget.getTitle()); } - @After - public void tearDown() { - UI.setCurrent(null); + @Test + public void setTitleNull_returnsEmptyTitle() { + DashboardWidget widget = new DashboardWidget(); + widget.setTitle("New title"); + widget.setTitle(null); + Assert.assertEquals("", widget.getTitle()); } @Test public void addWidgetToLayout_widgetIsAdded() { Div layout = new Div(); - ui.add(layout); + getUi().add(layout); DashboardWidget widget = new DashboardWidget(); layout.add(widget); fakeClientCommunication(); @@ -50,7 +52,7 @@ public void addWidgetToLayout_widgetIsAdded() { @Test public void removeWidgetFromLayout_widgetIsRemoved() { Div layout = new Div(); - ui.add(layout); + getUi().add(layout); DashboardWidget widget = new DashboardWidget(); layout.add(widget); fakeClientCommunication(); @@ -62,7 +64,7 @@ public void removeWidgetFromLayout_widgetIsRemoved() { @Test public void addWidgetToLayout_removeFromParent_widgetIsRemoved() { Div layout = new Div(); - ui.add(layout); + getUi().add(layout); DashboardWidget widget = new DashboardWidget(); layout.add(widget); fakeClientCommunication(); @@ -74,12 +76,12 @@ public void addWidgetToLayout_removeFromParent_widgetIsRemoved() { @Test public void addWidgetFromLayoutToAnotherLayout_widgetIsMoved() { Div parent = new Div(); - ui.add(parent); + getUi().add(parent); DashboardWidget widget = new DashboardWidget(); parent.add(widget); fakeClientCommunication(); Div newParent = new Div(); - ui.add(newParent); + getUi().add(newParent); newParent.add(widget); fakeClientCommunication(); Assert.assertTrue(parent.getChildren().noneMatch(widget::equals)); @@ -247,10 +249,4 @@ public void setContentToWidgetWithHeader_contentAndHeaderCorrectlyRetrieved() { Assert.assertEquals(content, widget.getContent()); Assert.assertEquals(header, widget.getHeader()); } - - private void fakeClientCommunication() { - ui.getInternals().getStateTree().runExecutionsBeforeClientResponse(); - ui.getInternals().getStateTree().collectChanges(ignore -> { - }); - } }