fix: add GridStackLayout showcase view (#7)
CI / build-and-test (pull_request) Successful in 1m13s

Adds a /gridstack route demonstrating GridStackLayout: four default
widgets (line/bar/pie chart cards plus a usage hint), buttons to add and
remove widgets at runtime, a reset-layout button and a layout-change
status line. Layout is persisted per browser via the existing
localStorage support. The view gets its own SideNavItem in MainLayout
and full de/en/es translations.

Fixes an infinite recursion in GridStackLayout.add(Component): a
single-argument add(item) resolves to that overload rather than the
varargs one, so its delegation to add(item) called itself. It now
appends through the element API and passes a GridStackItem through
unwrapped.

Adds the first test sources (browserless UI tests) covering the view's
widget count and the add/remove buttons.

Closes #7

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYAcKoXKPN3rJ7nBroKZkc
This commit is contained in:
Pit Friedrich
2026-07-25 22:12:05 +02:00
parent 12761d53ca
commit 17396466a9
9 changed files with 268 additions and 4 deletions
+5 -1
View File
@@ -20,7 +20,11 @@ public enum Fa {
FORM("fa-solid", "fa-rectangle-list"),
TABLE("fa-solid", "fa-table"),
GLOBE("fa-solid", "fa-globe"),
SEARCH("fa-solid", "fa-magnifying-glass");
SEARCH("fa-solid", "fa-magnifying-glass"),
GRID("fa-solid", "fa-table-cells-large"),
ADD("fa-solid", "fa-plus"),
REMOVE("fa-solid", "fa-trash"),
RESET("fa-solid", "fa-arrow-rotate-left");
private final String[] classes;
@@ -106,10 +106,17 @@ public class GridStackLayout extends Component implements HasSize, HasStyle {
}
}
/** Convenience overload: wraps a plain component in a default-sized {@link GridStackItem}. */
/** Convenience overload: wraps a plain component in a default-sized
* {@link GridStackItem} (an item passed in directly is added as is).
* <p>
* Appends through the element API rather than calling {@code add(item)}:
* a single-argument call resolves to this overload, not the varargs one,
* so delegating would recurse into itself. */
public GridStackItem add(Component content) {
GridStackItem item = new GridStackItem(content);
add(item);
GridStackItem item = content instanceof GridStackItem existing
? existing
: new GridStackItem(content);
getElement().appendChild(item.getElement());
return item;
}
@@ -0,0 +1,145 @@
package com.example.views;
import com.example.components.BarChart;
import com.example.components.Card;
import com.example.components.Fa;
import com.example.components.GridStackItem;
import com.example.components.GridStackLayout;
import com.example.components.LineChart;
import com.example.components.PieChart;
import com.example.components.ApexChart;
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.button.ButtonVariant;
import com.vaadin.flow.component.html.Paragraph;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.HasDynamicTitle;
import com.vaadin.flow.router.Route;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.List;
/**
* Showcase for {@link GridStackLayout}: a dashboard of draggable/resizable
* cards whose layout is persisted per browser, plus controls to add, remove
* and reset widgets at runtime.
*/
@Route("gridstack")
public class GridStackView extends VerticalLayout implements HasDynamicTitle {
private static final String STORAGE_KEY = "gridstack-demo";
private final GridStackLayout grid = new GridStackLayout();
private final Deque<GridStackItem> extraWidgets = new ArrayDeque<>();
private final Span status = new Span();
private int extraWidgetCount;
public GridStackView() {
addClassName("dialect-content");
grid.setWidthFull();
grid.setStorageKey(STORAGE_KEY);
grid.addLayoutChangeListener(e -> status.setText(
getTranslation("gridstack.status", e.getPositions().size())));
grid.add(
new GridStackItem("revenue-trend", 0, 0, 6, 3,
new Card(getTranslation("card.revenueTrend"), lineChart())),
new GridStackItem("revenue-month", 6, 0, 6, 3,
new Card(getTranslation("card.revenueByMonth"), barChart())),
new GridStackItem("revenue-region", 0, 3, 5, 3,
new Card(getTranslation("card.revenueByRegion"), pieChart())),
new GridStackItem("hint", 5, 3, 7, 3,
new Card(getTranslation("card.gridstackHint"),
new Paragraph(getTranslation("gridstack.hint")))));
status.setText(getTranslation("gridstack.statusInitial"));
status.addClassName("dialect-muted");
add(toolbar(), grid);
}
private HorizontalLayout toolbar() {
Button add = new Button(getTranslation("gridstack.addWidget"), Fa.ADD.create(),
e -> addWidget());
add.addThemeVariants(ButtonVariant.LUMO_PRIMARY);
Button remove = new Button(getTranslation("gridstack.removeWidget"), Fa.REMOVE.create(),
e -> removeWidget());
Button reset = new Button(getTranslation("gridstack.reset"), Fa.RESET.create(),
e -> grid.resetLayout());
HorizontalLayout toolbar = new HorizontalLayout(add, remove, reset, status);
toolbar.setPadding(false);
toolbar.setWidthFull();
toolbar.setAlignItems(Alignment.CENTER);
return toolbar;
}
private void addWidget() {
// The id must stay stable across reloads for the saved layout to match
// it again, so it is derived from a counter rather than a random UUID.
extraWidgetCount++;
String title = getTranslation("gridstack.widget", extraWidgetCount);
GridStackItem item = new GridStackItem("extra-" + extraWidgetCount, 0, 0, 4, 2,
new Card(title, new Paragraph(getTranslation("gridstack.widgetText"))));
extraWidgets.push(item);
grid.add(item);
}
private void removeWidget() {
GridStackItem item = extraWidgets.poll();
if (item == null) {
return;
}
grid.remove(item);
extraWidgetCount--;
}
private Component lineChart() {
LineChart chart = new LineChart();
chart.setData(getTranslation("chart.revenueSeries"),
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0), months());
return sizeFull(chart);
}
private Component barChart() {
BarChart chart = new BarChart();
chart.setData(getTranslation("chart.revenueSeries"),
List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0), months());
return sizeFull(chart);
}
private Component pieChart() {
PieChart chart = new PieChart();
chart.setData(List.of(30.0, 40.0, 35.0, 50.0),
List.of(getTranslation("region.north"), getTranslation("region.south"),
getTranslation("region.east"), getTranslation("region.west")));
return sizeFull(chart);
}
/** Charts fill their grid item instead of using a fixed pixel height, so
* resizing a widget resizes the chart (grid-stack.ts fires a window
* resize on resizestop, which ApexCharts reflows on). */
private Component sizeFull(ApexChart chart) {
chart.setWidthFull();
chart.setHeight("100%");
return chart;
}
private List<String> months() {
return List.of(
getTranslation("month.jan"), getTranslation("month.feb"),
getTranslation("month.mar"), getTranslation("month.apr"),
getTranslation("month.may"), getTranslation("month.jun"));
}
@Override
public String getPageTitle() {
return getTranslation("page.gridstack");
}
}
@@ -49,6 +49,8 @@ public class MainLayout extends AppLayout {
Fa.FORM.create()));
nav.addItem(new SideNavItem(getTranslation("nav.table"), TableView.class,
Fa.TABLE.create()));
nav.addItem(new SideNavItem(getTranslation("nav.gridstack"), GridStackView.class,
Fa.GRID.create()));
addToDrawer(nav);
}