From 487c206411841954a82bd0337eecea78bdf8e392 Mon Sep 17 00:00:00 2001 From: Pit Friedrich Date: Tue, 28 Jul 2026 19:12:52 +0200 Subject: [PATCH] fix: merge GridStackView into DashboardView (#19) DashboardView and GridStackView rendered the same three charts from the same hardcoded data, so every chart factory method existed twice. The dashboard is now the GridStackLayout variant at @Route(""): - GridStackView deleted, its grid/toolbar/chart factories moved into DashboardView, which keeps the point-click notifications the old dashboard had. - Charts size to their widget (100%/100%) instead of a fixed 400px. - MainLayout has one dashboard entry; nav.gridstack/page.gridstack keys dropped from all three bundles. - Storage key is "dashboard" (was "gridstack-demo"), so a saved layout from the old route is not reused. - GridStackViewTest renamed to DashboardViewTest, plus a test asserting the three charts render. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69 --- CLAUDE.md | 2 +- .../java/com/example/views/DashboardView.java | 148 +++++++++++++----- .../java/com/example/views/GridStackView.java | 132 ---------------- .../java/com/example/views/MainLayout.java | 2 - .../vaadin-i18n/translations.properties | 2 - .../vaadin-i18n/translations_en.properties | 2 - .../vaadin-i18n/translations_es.properties | 2 - ...ckViewTest.java => DashboardViewTest.java} | 26 ++- 8 files changed, 130 insertions(+), 186 deletions(-) delete mode 100644 src/main/java/com/example/views/GridStackView.java rename src/test/java/com/example/views/{GridStackViewTest.java => DashboardViewTest.java} (84%) diff --git a/CLAUDE.md b/CLAUDE.md index d6b9cdc..c98f5e7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -39,7 +39,7 @@ Since `apex-chart.ts` renders into light DOM (`createRenderRoot()` returns `this **`GridStackLayout`/`GridStackItem`** (`components/`) wrap [gridstack.js](https://gridstack.js.org) 13.1.0 for draggable/resizable grids. Unlike `ApexChart`, the bridge (`frontend/components/grid-stack.ts`) is a plain `HTMLElement`, not a Lit component — its children are server-rendered `GridStackItem`s living in light DOM, and a Lit render root would fight gridstack for ownership of them. A `MutationObserver` calls `makeWidget`/`removeWidget` as Flow adds/removes children, so there's no explicit add/remove protocol to the client. Every `GridStackItem` needs a stable `gs-id` (auto-generated if not given) — `GridStackLayout.setStorageKey(...)` persists drag/resize state to browser `localStorage` keyed on it and restores by matching ids, so items lose their saved position if their id changes between reloads. Item styling (`.grid-stack-item-content`) extends the `--dialect-*` alias layer in `styles.css`, same as `.dialect-card`. -**Views** (`views/`): `MainLayout` (`@Layout`, applies to all routes) is the `AppLayout` shell — navbar + `SideNav` drawer, with one `SideNavItem` per route. Routes: `DashboardView` (`@Route("")`) wraps each chart in a `Card` (`components/Card.java`); `FormView` (`@Route("formular")`) demonstrates form controls bound via `Binder`; `TableView` (`@Route("tabelle")`) demonstrates a `Grid` over dummy data with a live text filter (`GridListDataView.addFilter`, `TextField` in `ValueChangeMode.EAGER`). New views should reuse `Card` to wrap their content rather than adding components directly, and get a matching `SideNavItem` in `MainLayout`. +**Views** (`views/`): `MainLayout` (`@Layout`, applies to all routes) is the `AppLayout` shell — navbar + `SideNav` drawer, with one `SideNavItem` per route. Routes: `DashboardView` (`@Route("")`) puts each chart in a `Card` (`components/Card.java`) inside a `GridStackItem` of a `GridStackLayout`, so widgets are draggable/resizable and the layout is persisted to `localStorage`; charts use `width: 100%` / `height: 100%` to fill their widget rather than a fixed pixel height; `FormView` (`@Route("formular")`) demonstrates form controls bound via `Binder`; `TableView` (`@Route("tabelle")`) demonstrates a `Grid` over dummy data with a live text filter (`GridListDataView.addFilter`, `TextField` in `ValueChangeMode.EAGER`). New views should reuse `Card` to wrap their content rather than adding components directly, and get a matching `SideNavItem` in `MainLayout`. **Styling**: `src/main/resources/META-INF/resources/styles.css` is the one project-level stylesheet (loaded via `@StyleSheet("styles.css")` in `Application.java`). It defines `--dialect-*` design tokens (Dialect design system: primary orange `#E86C00`, cool-gray background, card radius/shadow) and aliases them onto Aura's own CSS custom properties (`--aura-accent-color-*`, `--aura-background-color-*`, `--aura-orange`, `--aura-yellow`) rather than fighting the theme. Aura tokens use OKLCH + relative-color syntax and accept plain hex overrides. When restyling, prefer extending this alias layer over hardcoding new colors in components. diff --git a/src/main/java/com/example/views/DashboardView.java b/src/main/java/com/example/views/DashboardView.java index 5be787b..3da86f9 100644 --- a/src/main/java/com/example/views/DashboardView.java +++ b/src/main/java/com/example/views/DashboardView.java @@ -1,63 +1,135 @@ package com.example.views; +import com.example.components.ApexChart; 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.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.notification.Notification; +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.List; +/** + * The dashboard: a {@link GridStackLayout} of draggable/resizable cards whose + * layout is persisted per browser, plus controls to add widgets and reset the + * layout at runtime. Widgets are removed by their own close button (see + * {@link GridStackItem#setClosable(boolean)}), not from the toolbar. + */ @Route("") public class DashboardView extends VerticalLayout implements HasDynamicTitle { + private static final String STORAGE_KEY = "dashboard"; + + private final GridStackLayout grid = new GridStackLayout(); + private final Span status = new Span(); + private int extraWidgetCount; + public DashboardView() { addClassName("dialect-content"); - List months = List.of( + 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 reset = new Button(getTranslation("gridstack.reset"), Fa.RESET.create(), + e -> grid.resetLayout()); + + HorizontalLayout toolbar = new HorizontalLayout(add, 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. + // The counter only ever grows: closing a widget must not hand its id to + // the next one, or the new widget would inherit the closed one's saved + // position. + extraWidgetCount++; + String title = getTranslation("gridstack.widget", extraWidgetCount); + grid.add(new GridStackItem("extra-" + extraWidgetCount, 0, 0, 4, 2, + new Card(title, new Paragraph(getTranslation("gridstack.widgetText"))))); + } + + 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()); + chart.addPointClickListener(e -> Notification.show(getTranslation( + "chart.pointClick", e.getSeriesIndex(), e.getDataPointIndex()))); + 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()); + chart.addPointClickListener(e -> Notification.show(getTranslation( + "chart.pointClick", e.getSeriesIndex(), e.getDataPointIndex()))); + 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"))); + chart.addPointClickListener(e -> Notification.show( + getTranslation("chart.sliceClick", e.getDataPointIndex()))); + 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 months() { + return List.of( getTranslation("month.jan"), getTranslation("month.feb"), getTranslation("month.mar"), getTranslation("month.apr"), getTranslation("month.may"), getTranslation("month.jun")); - - LineChart lineChart = new LineChart(); - lineChart.setWidthFull(); - lineChart.setHeight("400px"); - lineChart.setData(getTranslation("chart.revenueSeries"), - List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0), - months); - lineChart.addPointClickListener(e -> { - Notification.show(getTranslation("chart.pointClick", - e.getSeriesIndex(), e.getDataPointIndex())); - }); - - BarChart barChart = new BarChart(); - barChart.setWidthFull(); - barChart.setHeight("400px"); - barChart.setData(getTranslation("chart.revenueSeries"), - List.of(30.0, 40.0, 35.0, 50.0, 49.0, 60.0), - months); - barChart.addPointClickListener(e -> { - Notification.show(getTranslation("chart.pointClick", - e.getSeriesIndex(), e.getDataPointIndex())); - }); - - PieChart pieChart = new PieChart(); - pieChart.setWidthFull(); - pieChart.setHeight("400px"); - pieChart.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"))); - pieChart.addPointClickListener(e -> { - Notification.show(getTranslation("chart.sliceClick", e.getDataPointIndex())); - }); - - add(new Card(getTranslation("card.revenueTrend"), lineChart), - new Card(getTranslation("card.revenueByMonth"), barChart), - new Card(getTranslation("card.revenueByRegion"), pieChart)); } @Override diff --git a/src/main/java/com/example/views/GridStackView.java b/src/main/java/com/example/views/GridStackView.java deleted file mode 100644 index 80c65b3..0000000 --- a/src/main/java/com/example/views/GridStackView.java +++ /dev/null @@ -1,132 +0,0 @@ -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.List; - -/** - * Showcase for {@link GridStackLayout}: a dashboard of draggable/resizable - * cards whose layout is persisted per browser, plus controls to add widgets and - * reset the layout at runtime. Widgets are removed by their own close button - * (see {@link GridStackItem#setClosable(boolean)}), not from the toolbar. - */ -@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 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 reset = new Button(getTranslation("gridstack.reset"), Fa.RESET.create(), - e -> grid.resetLayout()); - - HorizontalLayout toolbar = new HorizontalLayout(add, 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. - // The counter only ever grows: closing a widget must not hand its id to - // the next one, or the new widget would inherit the closed one's saved - // position. - extraWidgetCount++; - String title = getTranslation("gridstack.widget", extraWidgetCount); - grid.add(new GridStackItem("extra-" + extraWidgetCount, 0, 0, 4, 2, - new Card(title, new Paragraph(getTranslation("gridstack.widgetText"))))); - } - - 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 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"); - } -} diff --git a/src/main/java/com/example/views/MainLayout.java b/src/main/java/com/example/views/MainLayout.java index c9ccc7d..0ea2e4f 100644 --- a/src/main/java/com/example/views/MainLayout.java +++ b/src/main/java/com/example/views/MainLayout.java @@ -49,8 +49,6 @@ 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); } diff --git a/src/main/resources/vaadin-i18n/translations.properties b/src/main/resources/vaadin-i18n/translations.properties index 5e01b51..b3310a1 100644 --- a/src/main/resources/vaadin-i18n/translations.properties +++ b/src/main/resources/vaadin-i18n/translations.properties @@ -10,12 +10,10 @@ lang.es=Español nav.dashboard=Dashboard nav.form=Formular nav.table=Tabelle -nav.gridstack=Raster page.dashboard=Dashboard page.form=Formular page.table=Tabelle -page.gridstack=Raster-Layout card.revenueTrend=Umsatz-Entwicklung card.revenueByMonth=Umsatz nach Monat diff --git a/src/main/resources/vaadin-i18n/translations_en.properties b/src/main/resources/vaadin-i18n/translations_en.properties index 192a9bc..7688e25 100644 --- a/src/main/resources/vaadin-i18n/translations_en.properties +++ b/src/main/resources/vaadin-i18n/translations_en.properties @@ -10,12 +10,10 @@ lang.es=Español nav.dashboard=Dashboard nav.form=Form nav.table=Table -nav.gridstack=Grid page.dashboard=Dashboard page.form=Form page.table=Table -page.gridstack=Grid layout card.revenueTrend=Revenue Trend card.revenueByMonth=Revenue by Month diff --git a/src/main/resources/vaadin-i18n/translations_es.properties b/src/main/resources/vaadin-i18n/translations_es.properties index f3cdde7..b21364e 100644 --- a/src/main/resources/vaadin-i18n/translations_es.properties +++ b/src/main/resources/vaadin-i18n/translations_es.properties @@ -10,12 +10,10 @@ lang.es=Español nav.dashboard=Panel nav.form=Formulario nav.table=Tabla -nav.gridstack=Cuadrícula page.dashboard=Panel page.form=Formulario page.table=Tabla -page.gridstack=Diseño de cuadrícula card.revenueTrend=Evolución de ingresos card.revenueByMonth=Ingresos por mes diff --git a/src/test/java/com/example/views/GridStackViewTest.java b/src/test/java/com/example/views/DashboardViewTest.java similarity index 84% rename from src/test/java/com/example/views/GridStackViewTest.java rename to src/test/java/com/example/views/DashboardViewTest.java index 04daf7c..2a6c92e 100644 --- a/src/test/java/com/example/views/GridStackViewTest.java +++ b/src/test/java/com/example/views/DashboardViewTest.java @@ -1,8 +1,11 @@ package com.example.views; import com.example.Application; +import com.example.components.BarChart; import com.example.components.GridStackItem; import com.example.components.GridStackLayout; +import com.example.components.LineChart; +import com.example.components.PieChart; import com.vaadin.browserless.SpringBrowserlessTest; import com.vaadin.browserless.ViewPackages; import com.vaadin.browserless.internal.ElementUtilsKt; @@ -21,12 +24,12 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @SpringBootTest(classes = Application.class) -@ViewPackages(classes = GridStackView.class) -class GridStackViewTest extends SpringBrowserlessTest { +@ViewPackages(classes = DashboardView.class) +class DashboardViewTest extends SpringBrowserlessTest { @Test void view_rendersGridWithDefaultWidgets() { - navigate(GridStackView.class); + navigate(DashboardView.class); GridStackLayout grid = $view(GridStackLayout.class).first(); assertNotNull(grid); @@ -36,9 +39,18 @@ class GridStackViewTest extends SpringBrowserlessTest { assertTrue(layout.stream().allMatch(p -> p.w() > 0 && p.h() > 0)); } + @Test + void view_rendersTheThreeCharts() { + navigate(DashboardView.class); + + assertNotNull($view(LineChart.class).first()); + assertNotNull($view(BarChart.class).first()); + assertNotNull($view(PieChart.class).first()); + } + @Test void addAndCloseWidget_changesItemCount() { - navigate(GridStackView.class); + navigate(DashboardView.class); GridStackLayout grid = $view(GridStackLayout.class).first(); int initial = grid.getLayout().size(); @@ -52,7 +64,7 @@ class GridStackViewTest extends SpringBrowserlessTest { @Test void closeButton_removesTheClickedWidgetOnly() { - navigate(GridStackView.class); + navigate(DashboardView.class); GridStackLayout grid = $view(GridStackLayout.class).first(); GridStackItem first = $view(GridStackItem.class).first(); @@ -68,7 +80,7 @@ class GridStackViewTest extends SpringBrowserlessTest { @Test void closeListener_firesOnceItemIsDetached() { - navigate(GridStackView.class); + navigate(DashboardView.class); GridStackLayout grid = $view(GridStackLayout.class).first(); GridStackItem item = $view(GridStackItem.class).first(); @@ -83,7 +95,7 @@ class GridStackViewTest extends SpringBrowserlessTest { @Test void nonClosableItem_hasNoCloseButton() { - navigate(GridStackView.class); + navigate(DashboardView.class); GridStackItem item = $view(GridStackItem.class).first(); assertTrue(item.isClosable(), "items are closable by default"); -- 2.52.0