Closing a widget was instant and destructive - one misclick on the X lost its size and position. Close now shows a toast with an undo button that re-adds the very same widget at its captured gs-x/y/w/h, keeping its gs-id. Programmatic close() (fromClient=false) is skipped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HGRChzremwYCUctf2qMrQz
This commit is contained in:
@@ -19,6 +19,7 @@ import com.vaadin.flow.component.button.ButtonVariant;
|
||||
import com.vaadin.flow.component.dialog.Dialog;
|
||||
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;
|
||||
@@ -40,7 +41,8 @@ import java.util.Optional;
|
||||
* 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, and are
|
||||
* added back from the picker over {@link WidgetRegistry}.
|
||||
* added back from the picker over {@link WidgetRegistry} — or undone straight
|
||||
* from the toast every close spawns (see {@link #offerUndo}).
|
||||
* <p>
|
||||
* All widget numbers come from {@link ChartDataService} and every chart widget
|
||||
* is built by the registry; this view only decides where a widget sits and
|
||||
@@ -97,17 +99,18 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
||||
refreshKpiTile(kpi.id());
|
||||
}
|
||||
});
|
||||
grid.add(item);
|
||||
addWidgetToGrid(item);
|
||||
}
|
||||
context.addFilterChangeListener(this::updateKpiTiles);
|
||||
|
||||
grid.add(
|
||||
List.of(
|
||||
defaultWidget(WidgetRegistry.REVENUE_TREND, 0, 1, 6, 3),
|
||||
defaultWidget(WidgetRegistry.REVENUE_MONTH, 6, 1, 6, 3),
|
||||
defaultWidget(WidgetRegistry.REVENUE_REGION, 0, 4, 5, 3),
|
||||
new GridStackItem("hint", 5, 4, 7, 3,
|
||||
new Card(getTranslation("card.gridstackHint"),
|
||||
new Paragraph(getTranslation("gridstack.hint")))));
|
||||
new Paragraph(getTranslation("gridstack.hint")))))
|
||||
.forEach(this::addWidgetToGrid);
|
||||
|
||||
status.setText(getTranslation("gridstack.statusInitial"));
|
||||
status.addClassName("dialect-muted");
|
||||
@@ -160,10 +163,57 @@ public class DashboardView extends VerticalLayout implements HasDynamicTitle {
|
||||
// widget must not hand its id to the next one, or the new widget would
|
||||
// inherit the closed one's saved position.
|
||||
extraWidgetCount++;
|
||||
grid.add(widget(definition.type() + "-" + extraWidgetCount, definition,
|
||||
addWidgetToGrid(widget(definition.type() + "-" + extraWidgetCount, definition,
|
||||
0, 0, definition.width(), definition.height()));
|
||||
}
|
||||
|
||||
/** Adds an item to the grid and wires the undo toast onto its close — every
|
||||
* widget goes through here exactly once, so the close listener (and thus
|
||||
* the undo offer) is registered once per item, not once per re-add: an
|
||||
* undo hands the very same instance back to {@link #grid} directly. */
|
||||
private void addWidgetToGrid(GridStackItem item) {
|
||||
item.addCloseListener(this::offerUndo);
|
||||
grid.add(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Closing a widget is one misclick away from losing its position, and a
|
||||
* confirmation dialog on every close would be worse than the problem — so
|
||||
* this shows a dismissible undo toast instead. Skipped for programmatic
|
||||
* closes ({@link GridStackItem#close()}, e.g. from duplicate cleanup),
|
||||
* which is exactly what {@link GridStackItem.CloseEvent#isFromClient()}
|
||||
* tells apart.
|
||||
* <p>
|
||||
* Undo re-adds the very item that was closed — not a rebuilt copy — at the
|
||||
* {@link GridStackItem.Position} the event captured before detaching, so it
|
||||
* keeps its {@code gs-id} and its exact size/position rather than landing
|
||||
* in the grid's next free slot.
|
||||
*/
|
||||
private void offerUndo(GridStackItem.CloseEvent event) {
|
||||
if (!event.isFromClient()) {
|
||||
return;
|
||||
}
|
||||
GridStackItem item = event.getSource();
|
||||
GridStackItem.Position position = event.getPosition();
|
||||
|
||||
Notification toast = new Notification();
|
||||
toast.setDuration(8000);
|
||||
toast.setPosition(Notification.Position.BOTTOM_START);
|
||||
|
||||
Button undo = new Button(getTranslation("gridstack.undo"), e -> {
|
||||
item.setPosition(position.x(), position.y(), position.w(), position.h());
|
||||
grid.add(item);
|
||||
toast.close();
|
||||
});
|
||||
undo.addThemeVariants(ButtonVariant.LUMO_TERTIARY_INLINE);
|
||||
|
||||
HorizontalLayout content = new HorizontalLayout(
|
||||
new Span(getTranslation("gridstack.closed")), undo);
|
||||
content.setAlignItems(Alignment.CENTER);
|
||||
toast.add(content);
|
||||
toast.open();
|
||||
}
|
||||
|
||||
/** A widget of the initial set. Its id is the plain type — never handed out
|
||||
* again by {@link #addWidget(WidgetDefinition)}, which always suffixes a
|
||||
* counter — and it keeps the dashboard's own placement rather than the
|
||||
|
||||
Reference in New Issue
Block a user