fix: export widget data as CSV from the action menu (#28)
CI / build-and-test (pull_request) Successful in 2m26s
CI / build-and-test (pull_request) Successful in 2m26s
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
This commit is contained in:
@@ -4,10 +4,13 @@ import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.ComponentEvent;
|
||||
import com.vaadin.flow.component.ComponentEventListener;
|
||||
import com.vaadin.flow.component.contextmenu.MenuItem;
|
||||
import com.vaadin.flow.component.html.Anchor;
|
||||
import com.vaadin.flow.component.html.AttachmentType;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.Span;
|
||||
import com.vaadin.flow.component.menubar.MenuBar;
|
||||
import com.vaadin.flow.component.menubar.MenuBarVariant;
|
||||
import com.vaadin.flow.server.streams.DownloadHandler;
|
||||
import com.vaadin.flow.shared.Registration;
|
||||
|
||||
import java.util.Collections;
|
||||
@@ -34,7 +37,9 @@ import java.util.UUID;
|
||||
* and {@link Action#REMOVE} are handled here, the rest is only reported to
|
||||
* {@link #addActionListener(ComponentEventListener) action listeners} — what
|
||||
* "refresh" or "duplicate" means depends on the widget, which this component
|
||||
* knows nothing about.
|
||||
* knows nothing about. An entry that hands out a file rather than changing the
|
||||
* widget is wired with
|
||||
* {@link #setActionDownload(Action, DownloadHandler) setActionDownload}.
|
||||
*/
|
||||
public class GridStackItem extends Div {
|
||||
|
||||
@@ -51,6 +56,12 @@ public class GridStackItem extends Div {
|
||||
* button's. */
|
||||
public static final String ACTION_MENU_CLASS = "dialect-action-menu";
|
||||
|
||||
/** Marker class for the download link of a
|
||||
* {@link #setActionDownload(Action, DownloadHandler) download entry};
|
||||
* {@code styles.css} stretches it over the whole entry, so the whole row
|
||||
* is clickable and not just its caption. */
|
||||
public static final String ACTION_LINK_CLASS = "dialect-action-link";
|
||||
|
||||
/** Class set on the item itself while it is
|
||||
* {@link #setMaximized(boolean) maximized} — {@code styles.css} is what
|
||||
* actually lifts the item out of the grid. */
|
||||
@@ -66,6 +77,10 @@ public class GridStackItem extends Div {
|
||||
MAXIMIZE,
|
||||
/** Add another widget of the same kind. Reported only. */
|
||||
DUPLICATE,
|
||||
/** Download the data behind the widget. Reported only — the file itself
|
||||
* comes from the {@link #setActionDownload(Action, DownloadHandler)
|
||||
* download handler} the caller wired to this entry. */
|
||||
EXPORT,
|
||||
/** Same as the close button, from the menu. Handled here. */
|
||||
REMOVE
|
||||
}
|
||||
@@ -80,6 +95,7 @@ public class GridStackItem extends Div {
|
||||
private final MenuBar actionMenu = new MenuBar();
|
||||
private final Map<Action, MenuItem> actionItems = new EnumMap<>(Action.class);
|
||||
private final Map<Action, Span> actionCaptions = new EnumMap<>(Action.class);
|
||||
private final Map<Action, Anchor> actionLinks = new EnumMap<>(Action.class);
|
||||
private boolean closable = true;
|
||||
private boolean maximized;
|
||||
|
||||
@@ -151,6 +167,7 @@ public class GridStackItem extends Div {
|
||||
addActionItem(root, Action.REFRESH, Fa.REFRESH, "gridstack.refresh");
|
||||
addActionItem(root, Action.MAXIMIZE, Fa.MAXIMIZE, "gridstack.maximize");
|
||||
addActionItem(root, Action.DUPLICATE, Fa.DUPLICATE, "gridstack.duplicate");
|
||||
addActionItem(root, Action.EXPORT, Fa.EXPORT, "gridstack.export");
|
||||
addActionItem(root, Action.REMOVE, Fa.REMOVE, "gridstack.remove");
|
||||
|
||||
setActions(DEFAULT_ACTIONS);
|
||||
@@ -189,6 +206,36 @@ public class GridStackItem extends Div {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes an entry hand out a file: its caption becomes a download link over
|
||||
* {@code handler}, so picking it downloads instead of only firing an
|
||||
* {@link ActionEvent} (which it still does — a listener can react to the
|
||||
* export as well).
|
||||
* <p>
|
||||
* The handler is asked for its content when the entry is clicked, not here,
|
||||
* so a widget that has been re-fed in the meantime exports what it is
|
||||
* showing at that moment. Calling this again re-points the same link.
|
||||
*/
|
||||
public GridStackItem setActionDownload(Action action, DownloadHandler handler) {
|
||||
Anchor link = actionLinks.computeIfAbsent(action, key -> {
|
||||
Anchor anchor = new Anchor();
|
||||
anchor.addClassName(ACTION_LINK_CLASS);
|
||||
// Reparents the caption into the link: the entry keeps its icon and
|
||||
// its text, only the text is now what the browser downloads from.
|
||||
anchor.add(actionCaptions.get(key));
|
||||
actionItems.get(key).add(anchor);
|
||||
return anchor;
|
||||
});
|
||||
link.setHref(handler, AttachmentType.DOWNLOAD);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** The download link of an entry wired with
|
||||
* {@link #setActionDownload(Action, DownloadHandler)}, if it has one. */
|
||||
public Anchor getActionLink(Action action) {
|
||||
return actionLinks.get(action);
|
||||
}
|
||||
|
||||
/** Shows or hides a single menu entry — the widget types that support an
|
||||
* action differ, the menu does not. */
|
||||
public GridStackItem setActionEnabled(Action action, boolean enabled) {
|
||||
|
||||
Reference in New Issue
Block a user