The menu's button is one of the item's corner controls, but it was left at
its own size: a menu-bar button measures 64px, twice the 32px slot the
corner offsets in styles.css assume. Painted after the grip and overlapping
it completely, it took every pointer event meant for the grip — widgets
could not be dragged at all.
Sized to 28px like the grip and the close button, so the slot math holds.
The top offset goes back to 14px with it, the 11px having compensated for
the taller button.
Covered by a Playwright test that checks the grip is the element hit at its
own centre and then drags the widget by it; the existing test only proved
the opposite direction, that dragging the menu button moves nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
The ApexCharts toolbar stays hidden (DialectTheme), so the dashboard had no
export at all. Adds an "Als CSV exportieren" entry to the per-widget action
menu, served server-side from the same data the widget renders.
- CsvExport: the export table plus its CSV dialect (';' separator, CRLF,
UTF-8 BOM, locale-formatted numbers) and file-name slugging.
- GridStackItem.setActionDownload: turns a menu entry's caption into an
anchor over a DownloadHandler, so an entry can hand out a file.
- WidgetRegistry: keeps each widget's query, not only its result, so the
Export hook re-runs it — an export always matches the current filter.
- DashboardView: builds the download on click; file name is widget title +
period (revenue-trend-half-year.csv).
Chart image export is left as the follow-up the issue calls optional.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
A GridStackItem now carries an overflow menu next to the grip and the
close button: refresh, maximize, duplicate, remove. Maximize and remove
are handled by the item itself; refresh and duplicate are only reported,
since what they mean depends on the widget.
Maximizing only sets a class — the item's gs-* attributes and its
gridstack node are untouched, so restoring is by definition the position
it had.
The menu sits outside the drag-handle selector, like the close button, so
opening it never starts a drag; a Playwright test drags it to prove it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
The reporting period was baked into the data service, so nothing could
change what the dashboard shows without editing code.
- DashboardFilter (period + optional region) parameterises every
ChartDataService query; the no-arg overloads are the default filter.
- DashboardContext is the bus between the new DashboardFilterBar and the
widgets: charts subscribe in WidgetRegistry, KPI tiles in DashboardView.
- Updates go through updateData, so widgets patch in place instead of
being rebuilt; SparklineChart gained the same path.
- KpiData carries the number, the bundle the unit and number pattern, so
a KPI value can follow the filter and stay locale-formatted.
- Filter state is deliberately not persisted, unlike the grid layout.
Closes#24
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
"Widget hinzufügen" appended an empty placeholder card, so a widget closed
via its X was gone until a page reload. Widget types now live in a
WidgetRegistry (WidgetDefinition: type id, title key, default size,
content factory); the toolbar button opens a picker dialog over the
registry, and the dashboard builds its initial widgets from it too.
Chart construction moves from DashboardView into the registry factories,
which resolve translation keys through the chart component so they follow
the UI locale. Added widgets get an id of "<type>-<n>" from a
monotonically growing counter, so a closed widget's id is never handed to
a new one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
Chart and KPI numbers were literals in DashboardView, duplicated between
the line and bar chart, and only existed at construction time. Add a thin
data layer as the seam for later refresh/filter work:
- ChartSeries / KpiData records, immutable, carrying translation keys
rather than display text so the data layer stays free of a UI locale.
- ChartDataService interface plus an in-memory implementation returning
the previous hardcoded numbers, so nothing changes visually.
- DashboardView injects the service, lays out the KPI tiles from its
list (grid ids come from the data, so saved layouts still match), and
resolves the keys against the bundle.
Also fixes DashboardChartPlaywrightTest, red on main since the KPI tiles
landed: it patched the first apex-chart, which is now a sparkline with no
xaxis.categories, so the axis-chart patch was a structural change and
legitimately redrew the SVG. It now targets an axis chart.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
Adds a non-chart dashboard widget: a large value with its label, an
optional signed delta versus the previous period, and an optional
sparkline. Four tiles now sit above the charts, 3x1 each.
- KpiTile draws no surface of its own and fills the grid item's, styled
from the --dialect-* token layer; the delta's up/down colors are new
tokens rather than literals, so they follow the light/dark toggle.
- Value and delta share one row: stacking them costs a line the tile does
not have at its default height of one grid cell.
- SparklineChart is a chrome-free line chart built on DialectTheme, so it
shares the palette and font with the real charts.
- ApexCharts' sparkline.enabled drops the axes on the initial render but
updateOptions (how the theme toggle recolors a chart) brings the y-axis
labels back; they are hidden in CSS, which reaches the chart SVG since
it renders into light DOM.
The registry registration the issue asks for is left out: #21 is blocked,
so there is no widget picker to register with yet. The tiles are wired
into DashboardView the same way the chart widgets are, and their values
come from the translation bundle until the data service (#20) lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
Every data change went through renderChart, which rebuilt the chart from a
full option set: animations restarted and zoom/selection state was lost.
Add an updateData path that patches the live chart via ApexCharts'
updateSeries (and updateOptions only when categories/labels actually
change), exposed as AxisChart.updateData / PieChart.updateData. Before the
first render there is nothing to patch, so those fall back to setData.
The theme overlay is re-applied to the merged options, so a rebuild after
a detach starts from the patched data.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0124BiJikbhbiEsNfJxdWM69
Adds the first browser-driven test to the project: PlaywrightTestBase starts
the app on a random port and drives a headless Chromium against it, and
FormViewPlaywrightTest covers the form's rendering, binder validation and the
save/reset buttons.
The tests skip themselves via a JUnit assumption when no browser can be
launched, so runners without Playwright browsers stay green.
Closes#16
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue9ZtWUBQF4SuSHpzZ3zwq
Every GridStackItem now renders a close button next to its drag grip,
mirroring the grip's hover affordance. Closing detaches the item
server-side; grid-stack.ts already unregisters widgets via its
MutationObserver, so the layout is persisted without an extra protocol.
Closable is on by default and can be turned off per item with
setClosable(false); GridStackItem.CloseEvent lets views react.
The GridStackView "remove last" toolbar button is dropped — per-widget
close replaces it, so the view no longer tracks a widget stack. Its
counter now only grows, keeping a closed widget's id from being handed
to a new widget (which would inherit the saved position).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue9ZtWUBQF4SuSHpzZ3zwq
The layout was saved to localStorage, but restoring it did not reliably
reproduce it:
- restore() applied the saved nodes one by one via grid.update(). A later
item could collide with one already put back and push it off its saved
spot, and nothing moved it back — so the restored layout was not the one
the user left. Use grid.load(nodes, false) instead: it sorts the nodes,
removes them from the engine before re-placing them, and runs in a single
batch. addRemove is off because Flow owns which children exist.
- gridstack's save() omits w/h when they are 1. The manual restore passed
them through as undefined, and the server read them as 0 and wrote a
zero-sized widget back onto the item. load() re-applies gridstack's own
defaults; onLayoutChange now reads a missing w/h as 1.
- A drag followed immediately by navigating to another route detached the
element inside the 150ms persist debounce, dropping the pending save.
disconnectedCallback now flushes it to localStorage first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ue9ZtWUBQF4SuSHpzZ3zwq
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
Set German locale on TimePicker in FormView and wire TimePickerI18n
error messages (bad input, required, min, max) through the existing
vaadin-i18n translation bundles, mirroring the DatePicker.DatePickerI18n
setup already present in FormView.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYAcKoXKPN3rJ7nBroKZkc