Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce add and remove widget functionality to dashboard #6572

Merged
merged 21 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0ce030d
feat: introduce add and remove widget functionality to dashboard
ugur-vaadin Aug 23, 2024
42ac8c8
fix: add missing lines and run formatter
ugur-vaadin Aug 23, 2024
8c7be9b
fix: fix compilation error
ugur-vaadin Aug 23, 2024
b050a2f
chore: run formatter
ugur-vaadin Aug 23, 2024
fbeb49e
test: remove first and last widgets and use clickelementswithjs
ugur-vaadin Aug 26, 2024
73ede6b
refactor: make remove from parent use removevirtualchild for widgets
ugur-vaadin Aug 26, 2024
beea359
refactor: use append and remove with virtual children and remove from…
ugur-vaadin Aug 26, 2024
ef25b52
chore: remove unused code
ugur-vaadin Aug 26, 2024
193f851
refactor: merge attach handlers
ugur-vaadin Aug 26, 2024
846cffa
refactor: do not try to remove virtual child on detach
ugur-vaadin Aug 26, 2024
ca6690a
refactor: use widgets list to update detach listeners
ugur-vaadin Aug 26, 2024
00641de
test: add more unit tests for testing node ids on detach
ugur-vaadin Aug 26, 2024
b6b8e35
refactor: use dashboard remove when removing widget from parent
ugur-vaadin Aug 27, 2024
7eebb09
test: use clickelementwithjs without locating element
ugur-vaadin Aug 27, 2024
752873d
refactor: remove detach listeners
ugur-vaadin Aug 27, 2024
426c288
test: add tests for moving widgets between parents
ugur-vaadin Aug 27, 2024
7a6908e
test: add unit tests for moving widgets between components
ugur-vaadin Aug 27, 2024
1b69222
test: cleanup dashboard widget related unit tests
ugur-vaadin Aug 27, 2024
07ea60d
refactor: use add remove directly instead of using virtual children
ugur-vaadin Aug 27, 2024
ea1a1f8
refactor: simplify removing all widgets by reusing single remove logic
ugur-vaadin Aug 27, 2024
738953e
refactor: override getchildren
ugur-vaadin Aug 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,68 @@
*/
package com.vaadin.flow.component.dashboard.tests;

import java.util.List;

import com.vaadin.flow.component.dashboard.Dashboard;
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")
public class DashboardPage extends Div {

public DashboardPage() {
DashboardWidget widget1 = new DashboardWidget();
widget1.setTitle("Widget 1");
widget1.setId("widget-1");

DashboardWidget widget2 = new DashboardWidget();
widget2.setTitle("Widget 2");
widget2.setId("widget-2");

DashboardWidget widget3 = new DashboardWidget();
widget3.setTitle("Widget 3");
widget3.setId("widget-3");

Dashboard dashboard = new Dashboard();
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);
});
addWidgetAtIndex1.setId("add-widget-at-index-1");

NativeButton removeFirstAndLastWidgets = new NativeButton(
"Remove first and last widgets");
removeFirstAndLastWidgets.addClickListener(click -> {
List<DashboardWidget> currentWidgets = dashboard.getWidgets();
if (currentWidgets.isEmpty()) {
return;
}
int currentWidgetCount = currentWidgets.size();
if (currentWidgetCount == 1) {
dashboard.remove(dashboard.getWidgets().get(0));
} else {
dashboard.remove(dashboard.getWidgets().get(0),
dashboard.getWidgets().get(currentWidgetCount - 1));
}
});
removeFirstAndLastWidgets.setId("remove-first-and-last-widgets");

NativeButton removeAllWidgets = new NativeButton("Remove all widgets");
removeAllWidgets.addClickListener(click -> dashboard.removeAll());
removeAllWidgets.setId("remove-all-widgets");

add(addWidgetAtIndex1, removeFirstAndLastWidgets, removeAllWidgets,
dashboard);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@
*/
package com.vaadin.flow.component.dashboard.tests;

import java.util.Arrays;
import java.util.List;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import com.vaadin.flow.component.dashboard.testbench.DashboardElement;
import com.vaadin.flow.component.dashboard.testbench.DashboardWidgetElement;
import com.vaadin.flow.testutil.TestPath;
import com.vaadin.tests.AbstractComponentIT;

Expand All @@ -16,4 +25,43 @@
*/
@TestPath("vaadin-dashboard")
public class DashboardIT extends AbstractComponentIT {

private DashboardElement dashboardElement;

@Before
public void init() {
open();
dashboardElement = $(DashboardElement.class).waitForFirst();
}

@Test
public void addWidgets_widgetsAreCorrectlyAdded() {
assertWidgetsByTitle("Widget 1", "Widget 2", "Widget 3");
}

@Test
public void addWidgetsAtIndex1_widgetIsAddedIntoTheCorrectPlace() {
clickElementWithJs("add-widget-at-index-1");
assertWidgetsByTitle("Widget 1", "Widget at index 1", "Widget 2",
"Widget 3");
}

@Test
public void removeFirstAndLastWidgets_widgetsAreCorrectlyRemoved() {
clickElementWithJs("remove-first-and-last-widgets");
assertWidgetsByTitle("Widget 2");
}

@Test
public void removeAllWidgets_widgetsAreCorrectlyRemoved() {
clickElementWithJs("remove-all-widgets");
assertWidgetsByTitle();
}

private void assertWidgetsByTitle(String... expectedWidgetTitles) {
List<DashboardWidgetElement> widgets = dashboardElement.getWidgets();
List<String> widgetTitles = widgets.stream()
.map(DashboardWidgetElement::getTitle).toList();
Assert.assertEquals(Arrays.asList(expectedWidgetTitles), widgetTitles);
}
}
5 changes: 5 additions & 0 deletions vaadin-dashboard-flow-parent/vaadin-dashboard-flow/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
<groupId>com.vaadin</groupId>
<artifactId>flow-html-components</artifactId>
</dependency>
<dependency>
<groupId>com.vaadin</groupId>
<artifactId>vaadin-renderer-flow</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down
tomivirkki marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,29 @@
*/
package com.vaadin.flow.component.dashboard;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.slf4j.LoggerFactory;

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
Expand All @@ -20,6 +39,214 @@
@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.js")
@JsModule("./flow-component-renderer.js")
// @NpmPackage(value = "@vaadin/dashboard", version = "24.6.0-alpha0")
public class Dashboard extends Component {

private final List<DashboardWidget> widgets = new ArrayList<>();

private boolean pendingUpdate = false;

/**
* Creates an empty dashboard.
*/
public Dashboard() {
}

/**
* Returns the widgets in the dashboard.
*
* @return The widgets in the dashboard
*/
public List<DashboardWidget> getWidgets() {
return Collections.unmodifiableList(widgets);
}

/**
* Adds the given widgets to the dashboard.
*
* @param widgets
* the widgets to add, not {@code null}
*/
public void add(DashboardWidget... widgets) {
Objects.requireNonNull(widgets, "Widgets to add cannot be null.");
List<DashboardWidget> 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();
}

/**
* Adds the given widget as child of this dashboard at the specific index.
* <p>
* 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}
*/
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.");
}
// The case when the index is bigger than the children count is handled
// inside the method below
doAddWidget(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
*/
public void remove(DashboardWidget... widgets) {
Objects.requireNonNull(widgets, "Widgets to remove cannot be null.");
List<DashboardWidget> 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 dashboard");
}
}
toRemove.forEach(this::doRemoveWidget);
updateClient();
}

/**
* Removes all widgets from this dashboard.
*/
public void removeAll() {
doRemoveAllWidgets();
updateClient();
}

private final Map<Element, Registration> 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();
});
}
};

@Override
protected void onAttach(AttachEvent attachEvent) {
super.onAttach(attachEvent);
attachRenderer();
doUpdateClient();
}

private void updateClient() {
if (pendingUpdate) {
return;
}
pendingUpdate = true;
getElement().getNode()
.runWhenAttached(ui -> ui.beforeClientResponse(this, ctx -> {
doUpdateClient();
pendingUpdate = false;
}));
}

private void doUpdateClient() {
widgets.forEach(widget -> {
Element childWidgetElement = widget.getElement();
if (!childDetachListenerMap.containsKey(childWidgetElement)) {
childDetachListenerMap.put(childWidgetElement,
childWidgetElement
.addDetachListener(childDetachListener));
tomivirkki marked this conversation as resolved.
Show resolved Hide resolved
}
});
getElement().setPropertyJson("items", createItemsJsonArray());
}

private void attachRenderer() {
getElement().executeJs(
"Vaadin.FlowComponentHost.patchVirtualContainer(this);");
tomivirkki marked this conversation as resolved.
Show resolved Hide resolved
String appId = UI.getCurrent().getInternals().getAppId();
getElement().executeJs(
"this.renderer = (root, _, model) => Vaadin.FlowComponentHost.setChildNodes($0, [model.item.nodeid], root);",
appId);
}

private JsonArray createItemsJsonArray() {
JsonArray jsonItems = Json.createArray();
for (DashboardWidget widget : widgets) {
JsonObject jsonItem = Json.createObject();
jsonItem.put("nodeid", getWidgetNodeId(widget));
jsonItems.set(jsonItems.length(), jsonItem);
}
return jsonItems;
}

private int getWidgetNodeId(DashboardWidget widget) {
return widget.getElement().getNode().getId();
}

private void doRemoveAllWidgets() {
List<Element> elementsToRemove = widgets.stream()
.map(Component::getElement).toList();
elementsToRemove.forEach(getElement()::removeVirtualChild);
widgets.clear();
tomivirkki marked this conversation as resolved.
Show resolved Hide resolved
}

private void doRemoveWidget(DashboardWidget widget) {
getElement().removeVirtualChild(widget.getElement());
widgets.remove(widget);
}

private void doAddWidget(int index, DashboardWidget widget) {
if (widget.getParent().isPresent()) {
widget.removeFromParent();
}
getElement().appendVirtualChild(widget.getElement());
widgets.add(index, widget);
}

private void doAddWidget(DashboardWidget widget) {
if (widget.getParent().isPresent()) {
widget.removeFromParent();
}
getElement().appendVirtualChild(widget.getElement());
widgets.add(widget);
}
}
Loading