diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardPage.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardPage.java index ae96b0cda33..b1ce7d0e624 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardPage.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/main/java/com/vaadin/flow/component/dashboard/tests/DashboardPage.java @@ -9,8 +9,10 @@ package com.vaadin.flow.component.dashboard.tests; import java.util.List; +import java.util.Optional; 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; @@ -23,6 +25,8 @@ public class DashboardPage extends Div { public DashboardPage() { + Dashboard dashboard = new Dashboard(); + DashboardWidget widget1 = new DashboardWidget(); widget1.setTitle("Widget 1"); widget1.setId("widget-1"); @@ -35,18 +39,39 @@ public DashboardPage() { widget3.setTitle("Widget 3"); widget3.setId("widget-3"); - Dashboard dashboard = new Dashboard(); + DashboardWidget widget1InSection1 = new DashboardWidget(); + widget1InSection1.setTitle("Widget 1 in Section 1"); + widget1InSection1.setId("widget-1-in-section-1"); + + DashboardWidget widget2InSection1 = new DashboardWidget(); + widget2InSection1.setTitle("Widget 2 in Section 1"); + widget2InSection1.setId("widget-2-in-section-1"); + + DashboardWidget widget1InSection2 = new DashboardWidget(); + widget1InSection2.setTitle("Widget 1 in Section 2"); + widget1InSection2.setId("widget-1-in-section-2"); + dashboard.add(widget1, widget2, widget3); - NativeButton addWidgetAtIndex1 = new NativeButton( - "Add widget at index 1"); - addWidgetAtIndex1.addClickListener(click -> { - DashboardWidget widgetAtIndex1 = new DashboardWidget(); - widgetAtIndex1.setTitle("Widget at index 1"); - widgetAtIndex1.setId("widget-at-index-1"); - dashboard.addWidgetAtIndex(1, widgetAtIndex1); + DashboardSection section1 = new DashboardSection("Section 1"); + section1.add(widget1InSection1, widget2InSection1); + dashboard.addSection(section1); + + DashboardSection section2 = dashboard.addSection("Section 2"); + section2.add(widget1InSection2); + + NativeButton addMultipleWidgets = new NativeButton( + "Add multiple widgets"); + addMultipleWidgets.addClickListener(click -> { + DashboardWidget newWidget1 = new DashboardWidget(); + newWidget1.setTitle("New widget 1"); + DashboardWidget newWidget2 = new DashboardWidget(); + newWidget2.setTitle("New widget 2"); + dashboard.add(newWidget2); + dashboard.addWidgetAtIndex( + (int) (dashboard.getChildren().count() - 1), newWidget1); }); - addWidgetAtIndex1.setId("add-widget-at-index-1"); + addMultipleWidgets.setId("add-multiple-widgets"); NativeButton removeFirstAndLastWidgets = new NativeButton( "Remove first and last widgets"); @@ -57,17 +82,69 @@ public DashboardPage() { } int currentWidgetCount = currentWidgets.size(); if (currentWidgetCount == 1) { - dashboard.remove(dashboard.getWidgets().get(0)); + dashboard.getWidgets().get(0).removeFromParent(); } else { - dashboard.remove(dashboard.getWidgets().get(0), - dashboard.getWidgets().get(currentWidgetCount - 1)); + dashboard.getWidgets().get(currentWidgetCount - 1) + .removeFromParent(); + dashboard.getWidgets().get(0).removeFromParent(); } }); removeFirstAndLastWidgets.setId("remove-first-and-last-widgets"); - NativeButton removeAllWidgets = new NativeButton("Remove all widgets"); - removeAllWidgets.addClickListener(click -> dashboard.removeAll()); - removeAllWidgets.setId("remove-all-widgets"); + NativeButton removeAll = new NativeButton("Remove all"); + removeAll.addClickListener(click -> dashboard.removeAll()); + removeAll.setId("remove-all"); + + NativeButton addSectionWithMultipleWidgets = new NativeButton( + "Add section with multiple widgets"); + addSectionWithMultipleWidgets.addClickListener(click -> { + DashboardSection section = dashboard + .addSection("New section with multiple widgets"); + DashboardWidget newWidget1 = new DashboardWidget(); + newWidget1.setTitle("New widget 1"); + DashboardWidget newWidget2 = new DashboardWidget(); + newWidget2.setTitle("New widget 2"); + section.add(newWidget2); + section.addWidgetAtIndex(0, newWidget1); + }); + addSectionWithMultipleWidgets + .setId("add-section-with-multiple-widgets"); + + NativeButton removeFirstSection = new NativeButton( + "Remove first section"); + removeFirstSection.addClickListener(click -> getFirstSection(dashboard) + .ifPresent(dashboard::remove)); + removeFirstSection.setId("remove-first-section"); + + NativeButton addWidgetToFirstSection = new NativeButton( + "Add widget to first section"); + addWidgetToFirstSection.addClickListener( + click -> getFirstSection(dashboard).ifPresent(section -> { + DashboardWidget newWidget = new DashboardWidget(); + newWidget.setTitle("New widget"); + section.add(newWidget); + })); + addWidgetToFirstSection.setId("add-widget-to-first-section"); + + NativeButton removeFirstWidgetFromFirstSection = new NativeButton( + "Remove first widget from first section"); + removeFirstWidgetFromFirstSection.addClickListener( + click -> getFirstSection(dashboard).ifPresent(section -> { + List currentWidgets = section.getWidgets(); + if (currentWidgets.isEmpty()) { + return; + } + section.remove(currentWidgets.get(0)); + })); + removeFirstWidgetFromFirstSection + .setId("remove-first-widget-from-first-section"); + + NativeButton removeAllFromFirstSection = new NativeButton( + "Remove all from first section"); + removeAllFromFirstSection + .addClickListener(click -> getFirstSection(dashboard) + .ifPresent(DashboardSection::removeAll)); + removeAllFromFirstSection.setId("remove-all-from-first-section"); NativeButton setMaximumColumnCount1 = new NativeButton( "Set maximum column count 1"); @@ -93,8 +170,18 @@ public DashboardPage() { .forEach(widget -> widget.setColspan(widget.getColspan() - 1))); decreaseAllColspansBy1.setId("decrease-all-colspans-by-1"); - add(addWidgetAtIndex1, removeFirstAndLastWidgets, removeAllWidgets, - setMaximumColumnCount1, setMaximumColumnCountNull, - increaseAllColspansBy1, decreaseAllColspansBy1, dashboard); + add(addMultipleWidgets, removeFirstAndLastWidgets, removeAll, + addSectionWithMultipleWidgets, removeFirstSection, + addWidgetToFirstSection, removeFirstWidgetFromFirstSection, + removeAllFromFirstSection, setMaximumColumnCount1, + setMaximumColumnCountNull, increaseAllColspansBy1, + decreaseAllColspansBy1, dashboard); + } + + private static Optional getFirstSection( + Dashboard dashboard) { + return dashboard.getChildren() + .filter(DashboardSection.class::isInstance) + .map(DashboardSection.class::cast).findFirst(); } } diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardIT.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardIT.java index d45e56a2fc8..bf9be05de7d 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardIT.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow-integration-tests/src/test/java/com/vaadin/flow/component/dashboard/tests/DashboardIT.java @@ -16,6 +16,7 @@ import org.junit.Test; import com.vaadin.flow.component.dashboard.testbench.DashboardElement; +import com.vaadin.flow.component.dashboard.testbench.DashboardSectionElement; import com.vaadin.flow.component.dashboard.testbench.DashboardWidgetElement; import com.vaadin.flow.testutil.TestPath; import com.vaadin.tests.AbstractComponentIT; @@ -35,27 +36,96 @@ public void init() { } @Test - public void addWidgets_widgetsAreCorrectlyAdded() { - assertWidgetsByTitle("Widget 1", "Widget 2", "Widget 3"); + public void addInitialWidgetsToDashboard_widgetsAreCorrectlyAdded() { + assertDashboardWidgetsByTitle("Widget 1", "Widget 2", "Widget 3", + "Widget 1 in Section 1", "Widget 2 in Section 1", + "Widget 1 in Section 2"); } @Test - public void addWidgetsAtIndex1_widgetIsAddedIntoTheCorrectPlace() { - clickElementWithJs("add-widget-at-index-1"); - assertWidgetsByTitle("Widget 1", "Widget at index 1", "Widget 2", - "Widget 3"); + public void addWidgetsToDashboard_widgetsAreCorrectlyAdded() { + clickElementWithJs("add-multiple-widgets"); + assertDashboardWidgetsByTitle("Widget 1", "Widget 2", "Widget 3", + "Widget 1 in Section 1", "Widget 2 in Section 1", + "Widget 1 in Section 2", "New widget 1", "New widget 2"); } @Test - public void removeFirstAndLastWidgets_widgetsAreCorrectlyRemoved() { + public void removeFirstAndLastWidgetsFromDashboard_widgetsAreCorrectlyRemoved() { clickElementWithJs("remove-first-and-last-widgets"); - assertWidgetsByTitle("Widget 2"); + assertDashboardWidgetsByTitle("Widget 2", "Widget 3", + "Widget 1 in Section 1", "Widget 2 in Section 1"); } @Test - public void removeAllWidgets_widgetsAreCorrectlyRemoved() { - clickElementWithJs("remove-all-widgets"); - assertWidgetsByTitle(); + public void removeAllFromDashboard_widgetsAnsSectionsAreCorrectlyRemoved() { + clickElementWithJs("remove-all"); + assertDashboardWidgetsByTitle(); + Assert.assertTrue(dashboardElement.getSections().isEmpty()); + } + + @Test + public void addInitialSectionsWithWidgetsToDashboard_widgetsAreCorrectlyAdded() { + List sections = dashboardElement.getSections(); + Assert.assertEquals(2, sections.size()); + + DashboardSectionElement section1 = sections.get(0); + Assert.assertEquals("Section 1", section1.getTitle()); + assertSectionWidgetsByTitle(section1, "Widget 1 in Section 1", + "Widget 2 in Section 1"); + + DashboardSectionElement section2 = sections.get(1); + Assert.assertEquals("Section 2", section2.getTitle()); + assertSectionWidgetsByTitle(section2, "Widget 1 in Section 2"); + } + + @Test + public void addSectionWithWidgets_sectionAndWidgetsAreCorrectlyAdded() { + clickElementWithJs("add-section-with-multiple-widgets"); + List sections = dashboardElement.getSections(); + Assert.assertEquals(3, sections.size()); + DashboardSectionElement newSection = sections.get(2); + Assert.assertEquals("New section with multiple widgets", + newSection.getTitle()); + assertSectionWidgetsByTitle(newSection, "New widget 1", "New widget 2"); + } + + @Test + public void removeFirstSection_sectionIsRemoved() { + clickElementWithJs("remove-first-section"); + List sections = dashboardElement.getSections(); + Assert.assertEquals(1, sections.size()); + DashboardSectionElement firstSection = sections.get(0); + Assert.assertEquals("Section 2", firstSection.getTitle()); + assertSectionWidgetsByTitle(firstSection, "Widget 1 in Section 2"); + } + + @Test + public void addWidgetToFirstSection_widgetsAreAdded() { + clickElementWithJs("add-widget-to-first-section"); + List sections = dashboardElement.getSections(); + DashboardSectionElement firstSection = sections.get(0); + Assert.assertEquals("Section 1", firstSection.getTitle()); + assertSectionWidgetsByTitle(firstSection, "Widget 1 in Section 1", + "Widget 2 in Section 1", "New widget"); + } + + @Test + public void removeFirstWidgetFromFirstSection_widgetIsRemoved() { + clickElementWithJs("remove-first-widget-from-first-section"); + List sections = dashboardElement.getSections(); + DashboardSectionElement firstSection = sections.get(0); + Assert.assertEquals("Section 1", firstSection.getTitle()); + assertSectionWidgetsByTitle(firstSection, "Widget 2 in Section 1"); + } + + @Test + public void removeAllFromFirstSection_widgetsAreRemoved() { + clickElementWithJs("remove-all-from-first-section"); + List sections = dashboardElement.getSections(); + DashboardSectionElement firstSection = sections.get(0); + Assert.assertEquals("Section 1", firstSection.getTitle()); + assertSectionWidgetsByTitle(firstSection); } @Test @@ -103,9 +173,20 @@ public void updateColspans_colspansForAllWidgetsUpdated() { widget.getColspan())); } - private void assertWidgetsByTitle(String... expectedWidgetTitles) { - List widgets = dashboardElement.getWidgets(); - List widgetTitles = widgets.stream() + private void assertDashboardWidgetsByTitle(String... expectedWidgetTitles) { + assertWidgetsByTitle(dashboardElement.getWidgets(), + expectedWidgetTitles); + } + + private static void assertSectionWidgetsByTitle( + DashboardSectionElement section, String... expectedWidgetTitles) { + assertWidgetsByTitle(section.getWidgets(), expectedWidgetTitles); + } + + private static void assertWidgetsByTitle( + List actualWidgets, + String... expectedWidgetTitles) { + List widgetTitles = actualWidgets.stream() .map(DashboardWidgetElement::getTitle).toList(); Assert.assertEquals(Arrays.asList(expectedWidgetTitles), widgetTitles); } 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 5afcd7538e2..0102e8884b7 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.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import java.util.stream.Stream; import org.slf4j.LoggerFactory; @@ -21,17 +22,9 @@ import com.vaadin.flow.component.AttachEvent; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.Tag; -import com.vaadin.flow.component.UI; import com.vaadin.flow.component.dependency.JsModule; import com.vaadin.flow.component.dependency.NpmPackage; import com.vaadin.flow.dom.Element; -import com.vaadin.flow.dom.ElementDetachEvent; -import com.vaadin.flow.dom.ElementDetachListener; -import com.vaadin.flow.shared.Registration; - -import elemental.json.Json; -import elemental.json.JsonArray; -import elemental.json.JsonObject; /** * @author Vaadin Ltd @@ -42,9 +35,11 @@ @JsModule("@vaadin/dashboard/src/vaadin-dashboard.js") @JsModule("./flow-component-renderer.js") // @NpmPackage(value = "@vaadin/dashboard", version = "24.6.0-alpha0") -public class Dashboard extends Component { +public class Dashboard extends Component implements HasWidgets { + + private final List childrenComponents = new ArrayList<>(); - private final List widgets = new ArrayList<>(); + private final DashboardChildDetachHandler childDetachHandler; private boolean pendingUpdate = false; @@ -52,23 +47,53 @@ public class Dashboard extends Component { * Creates an empty dashboard. */ public Dashboard() { + childDetachHandler = getChildDetachHandler(); } /** - * Returns the widgets in the dashboard. + * Adds an empty section to this dashboard. + */ + public DashboardSection addSection() { + return addSection((String) null); + } + + /** + * Adds an empty section to this dashboard. * - * @return The widgets in the dashboard + * @param title + * the title of the section */ - public List getWidgets() { - return Collections.unmodifiableList(widgets); + public DashboardSection addSection(String title) { + DashboardSection dashboardSection = new DashboardSection(title); + addSection(dashboardSection); + return dashboardSection; } /** - * Adds the given widgets to the dashboard. + * Adds the given section to this dashboard. * - * @param widgets + * @param section * the widgets to add, not {@code null} */ + public void addSection(DashboardSection section) { + doAddSection(section); + updateClient(); + } + + @Override + public List getWidgets() { + List widgets = new ArrayList<>(); + childrenComponents.forEach(component -> { + if (component instanceof DashboardSection section) { + widgets.addAll(section.getWidgets()); + } else { + widgets.add((DashboardWidget) component); + } + }); + return Collections.unmodifiableList(widgets); + } + + @Override public void add(DashboardWidget... widgets) { Objects.requireNonNull(widgets, "Widgets to add cannot be null."); List toAdd = new ArrayList<>(widgets.length); @@ -80,42 +105,23 @@ public void add(DashboardWidget... widgets) { updateClient(); } - /** - * Adds the given widget as child of this dashboard at the specific index. - *

- * In case the specified widget has already been added to another parent, it - * will be removed from there and added to this one. - * - * @param index - * the index, where the widget will be added. The index must be - * non-negative and may not exceed the children count - * @param widget - * the widget to add, not {@code null} - */ + @Override public void addWidgetAtIndex(int index, DashboardWidget widget) { Objects.requireNonNull(widget, "Widget to add cannot be null."); if (index < 0) { throw new IllegalArgumentException( "Cannot add a widget with a negative index."); } - if (index > widgets.size()) { + if (index > childrenComponents.size()) { throw new IllegalArgumentException(String.format( - "Cannot add a widget with index %d when there are %d widgets", - index, widgets.size())); + "Cannot add a widget with index %d when there are %d children components", + index, childrenComponents.size())); } - doAddWidget(index, widget); + doAddWidgetAtIndex(index, widget); updateClient(); } - /** - * Removes the given widgets from this dashboard. - * - * @param widgets - * the widgets to remove, not {@code null} - * @throws IllegalArgumentException - * if there is a widget whose non {@code null} parent is not - * this dashboard - */ + @Override public void remove(DashboardWidget... widgets) { Objects.requireNonNull(widgets, "Widgets to remove cannot be null."); List toRemove = new ArrayList<>(widgets.length); @@ -134,15 +140,36 @@ public void remove(DashboardWidget... widgets) { + ") is not a child of this dashboard"); } } - toRemove.forEach(this::doRemoveWidget); + if (!toRemove.isEmpty()) { + toRemove.forEach(this::doRemoveWidget); + updateClient(); + } + } + + /** + * Removes the given section from this component. + * + * @param section + * the section to remove, not {@code null} + * @throws IllegalArgumentException + * if the non {@code null} parent of the section is not this + * component + */ + public void remove(DashboardSection section) { + Objects.requireNonNull(section, "Section to remove cannot be null."); + doRemoveSection(section); updateClient(); } /** - * Removes all widgets from this dashboard. + * Removes all widgets and sections from this component. */ + @Override public void removeAll() { - doRemoveAllWidgets(); + if (getChildren().findAny().isEmpty()) { + return; + } + doRemoveAll(); updateClient(); } @@ -250,13 +277,14 @@ public void setGap(String gap) { @Override public Stream getChildren() { - return getWidgets().stream().map(Component.class::cast); + return childrenComponents.stream(); } @Override protected void onAttach(AttachEvent attachEvent) { super.onAttach(attachEvent); - attachRenderer(); + getElement().executeJs( + "Vaadin.FlowComponentHost.patchVirtualContainer(this);"); doUpdateClient(); } @@ -272,84 +300,94 @@ void updateClient() { })); } - private final Map childDetachListenerMap = new HashMap<>(); - - // Must not use lambda here as that would break serialization. See - // https://github.com/vaadin/flow-components/issues/5597 - private final ElementDetachListener childDetachListener = new ElementDetachListener() { - @Override - public void onDetach(ElementDetachEvent e) { - var detachedElement = e.getSource(); - getWidgets().stream() - .filter(widget -> Objects.equals(detachedElement, - widget.getElement())) - .findAny().ifPresent(detachedWidget -> { - // The child was removed from the dashboard - - // Remove the registration for the child detach listener - childDetachListenerMap.get(detachedWidget.getElement()) - .remove(); - childDetachListenerMap - .remove(detachedWidget.getElement()); - - widgets.remove(detachedWidget); - updateClient(); - }); - } - }; - private void doUpdateClient() { - widgets.forEach(widget -> { - Element childWidgetElement = widget.getElement(); - if (!childDetachListenerMap.containsKey(childWidgetElement)) { - childDetachListenerMap.put(childWidgetElement, - childWidgetElement - .addDetachListener(childDetachListener)); - } - }); - getElement().setPropertyJson("items", createItemsJsonArray()); - } - - private void attachRenderer() { - getElement().executeJs( - "Vaadin.FlowComponentHost.patchVirtualContainer(this);"); - String appId = UI.getCurrent().getInternals().getAppId(); - getElement().executeJs( - "this.renderer = (root, _, model) => Vaadin.FlowComponentHost.setChildNodes($0, [model.item.nodeid], root);", - appId); + childDetachHandler.refreshListeners(); + updateClientItems(); } - private JsonArray createItemsJsonArray() { - JsonArray jsonItems = Json.createArray(); - for (DashboardWidget widget : widgets) { - JsonObject jsonItem = Json.createObject(); - jsonItem.put("nodeid", getWidgetNodeId(widget)); - jsonItem.put("colspan", widget.getColspan()); - jsonItems.set(jsonItems.length(), jsonItem); + private void updateClientItems() { + final AtomicInteger itemIndex = new AtomicInteger(); + List itemRepresentations = new ArrayList<>(); + List flatOrderedComponents = new ArrayList<>(); + for (Component component : childrenComponents) { + flatOrderedComponents.add(component); + String itemRepresentation; + if (component instanceof DashboardSection section) { + flatOrderedComponents.addAll(section.getWidgets()); + List sectionWidgets = section.getWidgets(); + int sectionIndex = itemIndex.getAndIncrement(); + String sectionWidgetsRepresentation = sectionWidgets.stream() + .map(widget -> getWidgetRepresentation(widget, + itemIndex.getAndIncrement())) + .collect(Collectors.joining(",")); + itemRepresentation = "{ component: $%d, items: [ %s ] }" + .formatted(sectionIndex, sectionWidgetsRepresentation); + } else { + itemRepresentation = getWidgetRepresentation( + (DashboardWidget) component, + itemIndex.getAndIncrement()); + } + itemRepresentations.add(itemRepresentation); } - return jsonItems; + String updateItemsSnippet = "this.items = [ %s ];" + .formatted(String.join(",", itemRepresentations)); + getElement().executeJs(updateItemsSnippet, + flatOrderedComponents.toArray(Component[]::new)); } - private int getWidgetNodeId(DashboardWidget widget) { - return widget.getElement().getNode().getId(); + private static String getWidgetRepresentation(DashboardWidget widget, + int itemIndex) { + return "{ component: $%d, colspan: %d }".formatted(itemIndex, + widget.getColspan()); } - private void doRemoveAllWidgets() { - new ArrayList<>(widgets).forEach(this::doRemoveWidget); + private void doRemoveAll() { + new ArrayList<>(childrenComponents).forEach(child -> { + if (child instanceof DashboardSection section) { + doRemoveSection(section); + } else { + doRemoveWidget((DashboardWidget) child); + } + }); } private void doRemoveWidget(DashboardWidget widget) { getElement().removeChild(widget.getElement()); - widgets.remove(widget); + childrenComponents.remove(widget); } - private void doAddWidget(int index, DashboardWidget widget) { + private void doAddWidgetAtIndex(int index, DashboardWidget widget) { getElement().appendChild(widget.getElement()); - widgets.add(index, widget); + childrenComponents.add(index, widget); } private void doAddWidget(DashboardWidget widget) { getElement().appendChild(widget.getElement()); - widgets.add(widget); + childrenComponents.add(widget); + } + + private void doAddSection(DashboardSection section) { + getElement().appendChild(section.getElement()); + childrenComponents.add(section); + } + + private void doRemoveSection(DashboardSection section) { + getElement().removeChild(section.getElement()); + childrenComponents.remove(section); + } + + private DashboardChildDetachHandler getChildDetachHandler() { + return new DashboardChildDetachHandler() { + @Override + void removeChild(Component child) { + childrenComponents.remove(child); + updateClient(); + } + + @Override + Collection getDirectChildren() { + return Dashboard.this.getChildren().toList(); + } + }; } } 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 new file mode 100644 index 00000000000..64cda277be1 --- /dev/null +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardChildDetachHandler.java @@ -0,0 +1,58 @@ +/** + * 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 java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +import com.vaadin.flow.component.Component; +import com.vaadin.flow.dom.Element; +import com.vaadin.flow.dom.ElementDetachEvent; +import com.vaadin.flow.dom.ElementDetachListener; +import com.vaadin.flow.shared.Registration; + +public abstract class DashboardChildDetachHandler + implements ElementDetachListener { + + private final Map childDetachListenerMap = new HashMap<>(); + + @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); + }); + } + + void refreshListeners() { + getDirectChildren().forEach(child -> { + Element childElement = child.getElement(); + if (!childDetachListenerMap.containsKey(childElement)) { + childDetachListenerMap.put(childElement, + childElement.addDetachListener(this)); + } + }); + } + + 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/DashboardSection.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardSection.java new file mode 100644 index 00000000000..f899904b324 --- /dev/null +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardSection.java @@ -0,0 +1,193 @@ +/** + * 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 java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; + +import org.slf4j.LoggerFactory; + +import com.vaadin.flow.component.Component; +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; + +/** + * @author Vaadin Ltd + */ +@Tag("vaadin-dashboard-section") +@NpmPackage(value = "@vaadin/polymer-legacy-adapter", version = "24.5.0-alpha8") +@JsModule("@vaadin/polymer-legacy-adapter/style-modules.js") +@JsModule("@vaadin/dashboard/src/vaadin-dashboard-section.js") +// @NpmPackage(value = "@vaadin/dashboard", version = "24.6.0-alpha0") +public class DashboardSection extends Component implements HasWidgets { + + private final List widgets = new ArrayList<>(); + + private final DashboardChildDetachHandler childDetachHandler; + + /** + * Creates an empty section. + */ + public DashboardSection() { + this(null); + } + + /** + * Creates an empty section with title. + * + * @param title + * the title to set + */ + public DashboardSection(String title) { + super(); + childDetachHandler = getChildDetachHandler(); + setTitle(title); + } + + /** + * Returns the title of the section. + * + * @return the {@code sectionTitle} property from the web component + */ + public String getTitle() { + return getElement().getProperty("sectionTitle"); + } + + /** + * Sets the title of the section. + * + * @param title + * the title to set + */ + public void setTitle(String title) { + getElement().setProperty("sectionTitle", title); + } + + @Override + public List getWidgets() { + return Collections.unmodifiableList(widgets); + } + + @Override + public Stream getChildren() { + return widgets.stream().map(Component.class::cast); + } + + @Override + public void add(DashboardWidget... widgets) { + Objects.requireNonNull(widgets, "Widgets to add cannot be null."); + List toAdd = new ArrayList<>(widgets.length); + for (DashboardWidget widget : widgets) { + Objects.requireNonNull(widget, "Widget to add cannot be null."); + toAdd.add(widget); + } + toAdd.forEach(this::doAddWidget); + updateClient(); + } + + @Override + public void addWidgetAtIndex(int index, DashboardWidget widget) { + Objects.requireNonNull(widget, "Widget to add cannot be null."); + if (index < 0) { + throw new IllegalArgumentException( + "Cannot add a widget with a negative index."); + } + if (index > getWidgets().size()) { + throw new IllegalArgumentException(String.format( + "Cannot add a widget with index %d when there are %d widgets", + index, getWidgets().size())); + } + doAddWidgetAtIndex(index, widget); + updateClient(); + } + + @Override + public void remove(DashboardWidget... widgets) { + Objects.requireNonNull(widgets, "Widgets to remove cannot be null."); + List toRemove = new ArrayList<>(widgets.length); + for (DashboardWidget widget : widgets) { + Objects.requireNonNull(widget, "Widget to remove cannot be null."); + Element parent = widget.getElement().getParent(); + if (parent == null) { + LoggerFactory.getLogger(getClass()).debug( + "Removal of a widget with no parent does nothing."); + continue; + } + if (getElement().equals(parent)) { + toRemove.add(widget); + } else { + throw new IllegalArgumentException("The given widget (" + widget + + ") is not a child of this section"); + } + } + if (!toRemove.isEmpty()) { + toRemove.forEach(this::doRemoveWidget); + updateClient(); + } + } + + @Override + public void removeAll() { + if (getWidgets().isEmpty()) { + return; + } + doRemoveAll(); + updateClient(); + } + + @Override + public void removeFromParent() { + getParent().ifPresent(parent -> ((Dashboard) parent).remove(this)); + } + + private void doRemoveAll() { + new ArrayList<>(widgets).forEach(this::doRemoveWidget); + } + + private void doRemoveWidget(DashboardWidget widget) { + getElement().removeChild(widget.getElement()); + widgets.remove(widget); + } + + private void doAddWidgetAtIndex(int index, DashboardWidget widget) { + getElement().appendChild(widget.getElement()); + widgets.add(index, widget); + } + + private void doAddWidget(DashboardWidget widget) { + getElement().appendChild(widget.getElement()); + widgets.add(widget); + } + + void updateClient() { + childDetachHandler.refreshListeners(); + getParent().ifPresent(parent -> ((Dashboard) parent).updateClient()); + } + + private DashboardChildDetachHandler getChildDetachHandler() { + return new DashboardChildDetachHandler() { + @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/main/java/com/vaadin/flow/component/dashboard/DashboardWidget.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardWidget.java index 40fe6264cb9..8c85a840ed4 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardWidget.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/DashboardWidget.java @@ -68,13 +68,15 @@ public void setColspan(int colspan) { return; } this.colspan = colspan; - notifyParentDashboard(); + notifyParentDashboardOrSection(); } - private void notifyParentDashboard() { + private void notifyParentDashboardOrSection() { getParent().ifPresent(parent -> { if (parent instanceof Dashboard dashboard) { dashboard.updateClient(); + } else if (parent instanceof DashboardSection section) { + section.updateClient(); } }); } diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/HasWidgets.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/HasWidgets.java new file mode 100644 index 00000000000..63fcdb49a6a --- /dev/null +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-flow/src/main/java/com/vaadin/flow/component/dashboard/HasWidgets.java @@ -0,0 +1,60 @@ +/** + * 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 java.io.Serializable; +import java.util.List; + +public interface HasWidgets extends Serializable { + + /** + * Returns the widgets in this component. + * + * @return The widgets in this component + */ + List getWidgets(); + + /** + * Adds the given widgets to this component. + * + * @param widgets + * the widgets to add, not {@code null} + */ + void add(DashboardWidget... widgets); + + /** + * Adds the given widget as child of this component at the specific index. + *

+ * In case the specified widget has already been added to another parent, it + * will be removed from there and added to this one. + * + * @param index + * the index, where the widget will be added. The index must be + * non-negative and may not exceed the children count + * @param widget + * the widget to add, not {@code null} + */ + void addWidgetAtIndex(int index, DashboardWidget widget); + + /** + * Removes the given widgets from this component. + * + * @param widgets + * the widgets to remove, not {@code null} + * @throws IllegalArgumentException + * if there is a widget whose non {@code null} parent is not + * this component + */ + void remove(DashboardWidget... widgets); + + /** + * Removes all widgets from this component. + */ + void removeAll(); +} 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 87a82a0fc25..c48d59c39eb 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,9 +8,9 @@ */ package com.vaadin.flow.component.dashboard.tests; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.stream.Stream; import org.junit.After; import org.junit.Assert; @@ -21,16 +21,12 @@ 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.internal.JsonUtils; import com.vaadin.flow.server.VaadinSession; -import elemental.json.JsonArray; -import elemental.json.JsonObject; - public class DashboardTest { - private final UI ui = new UI(); private Dashboard dashboard; @@ -56,7 +52,7 @@ public void addWidget_widgetIsAdded() { DashboardWidget widget2 = new DashboardWidget(); dashboard.add(widget1, widget2); fakeClientCommunication(); - assertWidgets(dashboard, widget1, widget2); + assertChildComponents(dashboard, widget1, widget2); } @Test @@ -74,7 +70,7 @@ public void addNullWidgetInArray_noWidgetIsAdded() { // Do nothing } fakeClientCommunication(); - assertWidgets(dashboard); + assertChildComponents(dashboard); } @Test @@ -86,7 +82,7 @@ public void addWidgetAtIndex_widgetIsCorrectlyAdded() { fakeClientCommunication(); dashboard.addWidgetAtIndex(1, widget3); fakeClientCommunication(); - assertWidgets(dashboard, widget1, widget3, widget2); + assertChildComponents(dashboard, widget1, widget3, widget2); } @Test @@ -98,7 +94,7 @@ public void addWidgetAtInvalidIndex_exceptionIsThrown() { Assert.assertThrows(IllegalArgumentException.class, () -> dashboard.addWidgetAtIndex(2, widget2)); fakeClientCommunication(); - assertWidgets(dashboard, widget1); + assertChildComponents(dashboard, widget1); } @Test @@ -107,7 +103,7 @@ public void addWidgetAtNegativeIndex_exceptionIsThrown() { Assert.assertThrows(IllegalArgumentException.class, () -> dashboard.addWidgetAtIndex(-1, widget)); fakeClientCommunication(); - assertWidgets(dashboard); + assertChildComponents(dashboard); } @Test @@ -124,7 +120,7 @@ public void removeWidget_widgetIsRemoved() { fakeClientCommunication(); dashboard.remove(widget1); fakeClientCommunication(); - assertWidgets(dashboard, widget2); + assertChildComponents(dashboard, widget2); } @Test @@ -141,7 +137,7 @@ public void removeAllWidgets_widgetsAreRemoved() { fakeClientCommunication(); dashboard.removeAll(); fakeClientCommunication(); - assertWidgets(dashboard); + assertChildComponents(dashboard); } @Test @@ -151,7 +147,7 @@ public void removeWidgetFromParent_widgetIsRemoved() { fakeClientCommunication(); widget1.removeFromParent(); fakeClientCommunication(); - assertWidgets(dashboard); + assertChildComponents(dashboard); } @Test @@ -162,11 +158,11 @@ public void addMultipleWidgets_removeOneFromParent_widgetIsRemoved() { fakeClientCommunication(); widget1.removeFromParent(); fakeClientCommunication(); - assertWidgets(dashboard, widget2); + assertChildComponents(dashboard, widget2); } @Test - public void addSeparately_removeOneFromParent_widgetIsRemoved() { + public void addWidgetsSeparately_removeOneFromParent_widgetIsRemoved() { DashboardWidget widget1 = new DashboardWidget(); DashboardWidget widget2 = new DashboardWidget(); dashboard.add(widget1); @@ -174,7 +170,7 @@ public void addSeparately_removeOneFromParent_widgetIsRemoved() { fakeClientCommunication(); widget1.removeFromParent(); fakeClientCommunication(); - assertWidgets(dashboard, widget2); + assertChildComponents(dashboard, widget2); } @Test @@ -187,7 +183,7 @@ public void addWidgetFromLayoutToDashboard_widgetIsMoved() { dashboard.add(widget); fakeClientCommunication(); Assert.assertTrue(parent.getChildren().noneMatch(widget::equals)); - assertWidgets(dashboard, widget); + assertChildComponents(dashboard, widget); } @Test @@ -199,7 +195,7 @@ public void addWidgetFromDashboardToLayout_widgetIsMoved() { ui.add(parent); parent.add(widget); fakeClientCommunication(); - assertWidgets(dashboard); + assertChildComponents(dashboard); Assert.assertTrue(parent.getChildren().anyMatch(widget::equals)); } @@ -212,8 +208,381 @@ public void addWidgetToAnotherDashboard_widgetIsMoved() { ui.add(newDashboard); newDashboard.add(widget); fakeClientCommunication(); - assertWidgets(dashboard); - assertWidgets(newDashboard, widget); + assertChildComponents(dashboard); + assertChildComponents(newDashboard, widget); + } + + @Test + public void addSectionWithoutTitle_sectionIsAdded() { + DashboardSection section1 = dashboard.addSection(); + DashboardSection section2 = dashboard.addSection(); + fakeClientCommunication(); + assertChildComponents(dashboard, section1, section2); + } + + @Test + public void addSectionWithNullTitle_sectionIsAdded() { + DashboardSection section1 = dashboard.addSection((String) null); + DashboardSection section2 = dashboard.addSection((String) null); + fakeClientCommunication(); + assertChildComponents(dashboard, section1, section2); + } + + @Test + public void addSectionWithTitle_sectionIsAdded() { + DashboardSection section1 = dashboard.addSection("Section 1"); + DashboardSection section2 = dashboard.addSection("Section 2"); + fakeClientCommunication(); + assertChildComponents(dashboard, section1, section2); + } + + @Test + public void createAndAddSectionWithoutTitle_sectionIsAdded() { + DashboardSection section1 = new DashboardSection(); + DashboardSection section2 = new DashboardSection(); + dashboard.addSection(section1); + dashboard.addSection(section2); + fakeClientCommunication(); + assertChildComponents(dashboard, section1, section2); + } + + @Test + public void createAndAddSectionWithNullTitle_sectionIsAdded() { + DashboardSection section1 = new DashboardSection(null); + DashboardSection section2 = new DashboardSection(null); + dashboard.addSection(section1); + dashboard.addSection(section2); + fakeClientCommunication(); + assertChildComponents(dashboard, section1, section2); + } + + @Test + public void createAndAddSectionWithTitle_sectionIsAdded() { + DashboardSection section1 = new DashboardSection("Section 1"); + DashboardSection section2 = new DashboardSection("Section 2"); + dashboard.addSection(section1); + dashboard.addSection(section2); + fakeClientCommunication(); + assertChildComponents(dashboard, section1, section2); + } + + @Test + public void addNullSection_exceptionIsThrown() { + Assert.assertThrows(NullPointerException.class, + () -> dashboard.addSection((DashboardSection) null)); + } + + @Test + public void removeSection_sectionIsRemoved() { + DashboardSection section1 = dashboard.addSection(); + DashboardSection section2 = dashboard.addSection(); + fakeClientCommunication(); + dashboard.remove(section1); + fakeClientCommunication(); + assertChildComponents(dashboard, section2); + } + + @Test + public void removeNullSection_exceptionIsThrown() { + Assert.assertThrows(NullPointerException.class, + () -> dashboard.remove((DashboardSection) null)); + } + + @Test + public void removeAllSections_sectionsAreRemoved() { + dashboard.addSection(); + dashboard.addSection(); + fakeClientCommunication(); + dashboard.removeAll(); + fakeClientCommunication(); + assertChildComponents(dashboard); + } + + @Test + public void removeSectionFromParent_sectionIsRemoved() { + DashboardSection section = dashboard.addSection(); + fakeClientCommunication(); + section.removeFromParent(); + fakeClientCommunication(); + assertChildComponents(dashboard); + } + + @Test + public void addMultipleSections_removeOneFromParent_sectionIsRemoved() { + DashboardSection section1 = dashboard.addSection(); + DashboardSection section2 = dashboard.addSection(); + fakeClientCommunication(); + section1.removeFromParent(); + fakeClientCommunication(); + assertChildComponents(dashboard, section2); + } + + @Test + public void setTitleOnExistingSection_itemsAreUpdatedWithCorrectTitles() { + DashboardSection section = dashboard.addSection("Section"); + fakeClientCommunication(); + section.setTitle("New title"); + fakeClientCommunication(); + assertChildComponents(dashboard, section); + } + + @Test + public void addSectionWithWidget_removeWidgetFromDashboard_throwsException() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget = new DashboardWidget(); + section.add(widget); + fakeClientCommunication(); + Assert.assertThrows(IllegalArgumentException.class, + () -> dashboard.remove(widget)); + fakeClientCommunication(); + assertChildComponents(dashboard, section); + } + + @Test + public void addSection_addWidgetToSection_widgetIsAdded() { + DashboardSection section = dashboard.addSection(); + fakeClientCommunication(); + DashboardWidget widget = new DashboardWidget(); + section.add(widget); + fakeClientCommunication(); + assertChildComponents(dashboard, section); + } + + @Test + public void addSectionAndWidget_removeWidget_widgetRemoved() { + DashboardSection section = dashboard.addSection(); + section.add(new DashboardWidget()); + DashboardWidget widget = new DashboardWidget(); + dashboard.add(widget); + fakeClientCommunication(); + dashboard.remove(widget); + fakeClientCommunication(); + assertChildComponents(dashboard, section); + } + + @Test + public void addSectionAndWidget_removeSection_sectionRemoved() { + DashboardSection section = dashboard.addSection(); + section.add(new DashboardWidget()); + DashboardWidget widget = new DashboardWidget(); + dashboard.add(widget); + fakeClientCommunication(); + dashboard.remove(section); + fakeClientCommunication(); + assertChildComponents(dashboard, widget); + } + + @Test + public void addSectionAndWidget_removeAll_widgetAndSectionRemoved() { + DashboardSection section = dashboard.addSection(); + section.add(new DashboardWidget()); + DashboardWidget widget = new DashboardWidget(); + dashboard.add(widget); + fakeClientCommunication(); + dashboard.removeAll(); + fakeClientCommunication(); + assertChildComponents(dashboard); + } + + @Test + public void addWidgetToSection_widgetIsAdded() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget1 = new DashboardWidget(); + DashboardWidget widget2 = new DashboardWidget(); + section.add(widget1, widget2); + fakeClientCommunication(); + assertSectionWidgets(section, widget1, widget2); + assertChildComponents(dashboard, section); + + } + + @Test + public void addNullWidgetToSection_exceptionIsThrown() { + DashboardSection section = dashboard.addSection(); + Assert.assertThrows(NullPointerException.class, + () -> section.add((DashboardWidget) null)); + fakeClientCommunication(); + assertChildComponents(dashboard, section); + } + + @Test + public void addNullWidgetInArrayToSection_noWidgetIsAdded() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget = new DashboardWidget(); + try { + section.add(widget, null); + } catch (NullPointerException e) { + // Do nothing + } + fakeClientCommunication(); + assertSectionWidgets(section); + assertChildComponents(dashboard, section); + } + + @Test + public void addWidgetAtIndexToSection_widgetIsCorrectlyAdded() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget1 = new DashboardWidget(); + DashboardWidget widget2 = new DashboardWidget(); + DashboardWidget widget3 = new DashboardWidget(); + section.add(widget1, widget2); + fakeClientCommunication(); + section.addWidgetAtIndex(1, widget3); + fakeClientCommunication(); + assertSectionWidgets(section, widget1, widget3, widget2); + assertChildComponents(dashboard, section); + } + + @Test + public void addWidgetAtInvalidIndexToSection_exceptionIsThrown() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget1 = new DashboardWidget(); + DashboardWidget widget2 = new DashboardWidget(); + section.add(widget1); + fakeClientCommunication(); + Assert.assertThrows(IllegalArgumentException.class, + () -> section.addWidgetAtIndex(2, widget2)); + fakeClientCommunication(); + assertSectionWidgets(section, widget1); + assertChildComponents(dashboard, section); + } + + @Test + public void addWidgetAtNegativeIndexToSection_exceptionIsThrown() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget = new DashboardWidget(); + Assert.assertThrows(IllegalArgumentException.class, + () -> section.addWidgetAtIndex(-1, widget)); + fakeClientCommunication(); + assertSectionWidgets(section); + assertChildComponents(dashboard, section); + } + + @Test + public void addNullWidgetAtIndexToSection_exceptionIsThrown() { + DashboardSection section = dashboard.addSection(); + fakeClientCommunication(); + Assert.assertThrows(NullPointerException.class, + () -> section.addWidgetAtIndex(0, null)); + assertChildComponents(dashboard, section); + } + + @Test + public void removeWidgetFromSection_widgetIsRemoved() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget1 = new DashboardWidget(); + DashboardWidget widget2 = new DashboardWidget(); + section.add(widget1, widget2); + fakeClientCommunication(); + section.remove(widget1); + fakeClientCommunication(); + assertSectionWidgets(section, widget2); + assertChildComponents(dashboard, section); + } + + @Test + public void removeNullWidgetFromSection_exceptionIsThrown() { + DashboardSection section = dashboard.addSection(); + fakeClientCommunication(); + Assert.assertThrows(NullPointerException.class, + () -> section.remove((DashboardWidget) null)); + assertChildComponents(dashboard, section); + } + + @Test + public void removeAllWidgetsFromSection_widgetsAreRemoved() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget1 = new DashboardWidget(); + DashboardWidget widget2 = new DashboardWidget(); + section.add(widget1, widget2); + fakeClientCommunication(); + section.removeAll(); + fakeClientCommunication(); + assertSectionWidgets(section); + assertChildComponents(dashboard, section); + } + + @Test + public void removeWidgetInSectionFromParent_widgetIsRemoved() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget1 = new DashboardWidget(); + section.add(widget1); + fakeClientCommunication(); + widget1.removeFromParent(); + fakeClientCommunication(); + assertSectionWidgets(section); + assertChildComponents(dashboard, section); + } + + @Test + public void addMultipleWidgetsToSection_removeOneFromParent_widgetIsRemoved() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget1 = new DashboardWidget(); + DashboardWidget widget2 = new DashboardWidget(); + section.add(widget1, widget2); + fakeClientCommunication(); + widget1.removeFromParent(); + fakeClientCommunication(); + assertSectionWidgets(section, widget2); + assertChildComponents(dashboard, section); + } + + @Test + public void addWidgetsSeparatelyToSection_removeOneFromParent_widgetIsRemoved() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget1 = new DashboardWidget(); + DashboardWidget widget2 = new DashboardWidget(); + section.add(widget1); + section.add(widget2); + fakeClientCommunication(); + widget1.removeFromParent(); + fakeClientCommunication(); + assertSectionWidgets(section, widget2); + assertChildComponents(dashboard, section); + } + + @Test + public void addWidgetFromLayoutToSection_widgetIsMoved() { + DashboardSection section = dashboard.addSection(); + Div parent = new Div(); + ui.add(parent); + DashboardWidget widget = new DashboardWidget(); + parent.add(widget); + fakeClientCommunication(); + section.add(widget); + fakeClientCommunication(); + Assert.assertTrue(parent.getChildren().noneMatch(widget::equals)); + assertSectionWidgets(section, widget); + assertChildComponents(dashboard, section); + } + + @Test + public void addWidgetFromSectionToLayout_widgetIsMoved() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget = new DashboardWidget(); + section.add(widget); + fakeClientCommunication(); + Div parent = new Div(); + ui.add(parent); + parent.add(widget); + fakeClientCommunication(); + assertSectionWidgets(section); + Assert.assertTrue(parent.getChildren().anyMatch(widget::equals)); + assertChildComponents(dashboard, section); + } + + @Test + public void addWidgetToAnotherSection_widgetIsMoved() { + DashboardSection section = dashboard.addSection(); + DashboardWidget widget = new DashboardWidget(); + section.add(widget); + fakeClientCommunication(); + DashboardSection newSection = dashboard.addSection(); + newSection.add(widget); + fakeClientCommunication(); + assertSectionWidgets(section); + assertSectionWidgets(newSection, widget); + assertChildComponents(dashboard, section, newSection); } @Test @@ -262,7 +631,7 @@ public void setWidgetsWithDifferentColspans_itemsAreGeneratedWithCorrectColspans widget2.setColspan(2); dashboard.add(widget1, widget2); fakeClientCommunication(); - assertWidgetColspans(dashboard, widget1, widget2); + assertChildComponents(dashboard, widget1, widget2); } @Test @@ -272,7 +641,7 @@ public void setColspanOnExistingWidget_itemsAreUpdatedWithCorrectColspans() { fakeClientCommunication(); widget.setColspan(2); fakeClientCommunication(); - assertWidgetColspans(dashboard, widget); + assertChildComponents(dashboard, widget); } @Test @@ -432,49 +801,34 @@ private void fakeClientCommunication() { }); } - private static void assertWidgets(Dashboard dashboard, - DashboardWidget... expectedWidgets) { - assertVirtualChildren(dashboard, expectedWidgets); - Assert.assertEquals(Arrays.asList(expectedWidgets), - dashboard.getWidgets()); - } - - private static void assertVirtualChildren(Dashboard dashboard, - Component... components) { - // Get a List of the node ids - List expectedChildNodeIds = Arrays.stream(components) - .map(component -> component.getElement().getNode().getId()) - .toList(); - // Get the node ids from the items property of the dashboard - List actualChildNodeIds = getChildNodeIds(dashboard); - Assert.assertEquals(expectedChildNodeIds, actualChildNodeIds); - } - - private static List getChildNodeIds(Dashboard dashboard) { - return getItemsStream(dashboard) - .mapToInt(obj -> (int) obj.getNumber("nodeid")).boxed() - .toList(); - } - - private static void assertWidgetColspans(Dashboard dashboard, - DashboardWidget... widgets) { - // Get a List of the widget colspans - List expectedColspans = Arrays.stream(widgets) - .map(DashboardWidget::getColspan).toList(); - // Get the colspans from the items property of the dashboard - List actualColspans = getWidgetColspans(dashboard); - Assert.assertEquals(expectedColspans, actualColspans); - } - - private static List getWidgetColspans(Dashboard dashboard) { - return getItemsStream(dashboard) - .mapToInt(obj -> (int) obj.getNumber("colspan")).boxed() - .toList(); + 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; } - private static Stream getItemsStream(Dashboard dashboard) { - JsonArray jsonArrayOfIds = (JsonArray) dashboard.getElement() - .getPropertyRaw("items"); - return JsonUtils.objectStream(jsonArrayOfIds); + private static void assertSectionWidgets(DashboardSection section, + DashboardWidget... expectedWidgets) { + Assert.assertEquals(Arrays.asList(expectedWidgets), + section.getWidgets()); } } diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-testbench/src/main/java/com/vaadin/flow/component/dashboard/testbench/DashboardElement.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-testbench/src/main/java/com/vaadin/flow/component/dashboard/testbench/DashboardElement.java index 725f5b8008b..f8058447337 100644 --- a/vaadin-dashboard-flow-parent/vaadin-dashboard-testbench/src/main/java/com/vaadin/flow/component/dashboard/testbench/DashboardElement.java +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-testbench/src/main/java/com/vaadin/flow/component/dashboard/testbench/DashboardElement.java @@ -27,4 +27,13 @@ public class DashboardElement extends TestBenchElement { public List getWidgets() { return $(DashboardWidgetElement.class).all(); } + + /** + * Returns the sections in the dashboard. + * + * @return The sections in the dashboard + */ + public List getSections() { + return $(DashboardSectionElement.class).all(); + } } diff --git a/vaadin-dashboard-flow-parent/vaadin-dashboard-testbench/src/main/java/com/vaadin/flow/component/dashboard/testbench/DashboardSectionElement.java b/vaadin-dashboard-flow-parent/vaadin-dashboard-testbench/src/main/java/com/vaadin/flow/component/dashboard/testbench/DashboardSectionElement.java new file mode 100644 index 00000000000..e8023cda9ea --- /dev/null +++ b/vaadin-dashboard-flow-parent/vaadin-dashboard-testbench/src/main/java/com/vaadin/flow/component/dashboard/testbench/DashboardSectionElement.java @@ -0,0 +1,39 @@ +/** + * 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.testbench; + +import java.util.List; + +import com.vaadin.testbench.TestBenchElement; +import com.vaadin.testbench.elementsbase.Element; + +/** + * @author Vaadin Ltd + */ +@Element("vaadin-dashboard-section") +public class DashboardSectionElement extends TestBenchElement { + + /** + * Returns the title of the section. + * + * @return the {@code sectionTitle} property from the web component + */ + public String getTitle() { + return getPropertyString("sectionTitle"); + } + + /** + * Returns the widgets in the section. + * + * @return The widgets in the section + */ + public List getWidgets() { + return $(DashboardWidgetElement.class).all(); + } +}