Skip to content

Commit

Permalink
Introduce pending task toast and apply it on sample batch interaction (
Browse files Browse the repository at this point in the history
…#974)

Enables more complex toasts by allowing to add components to the toast.

Provides a method to create toasts for pending tasks.
  • Loading branch information
sven1103 authored Jan 15, 2025
1 parent fc0c1f6 commit 6dfe9dd
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 69 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package life.qbic.datamanager.views.demo;

import com.vaadin.flow.component.UI;
import com.vaadin.flow.component.Unit;
import com.vaadin.flow.component.button.Button;
import com.vaadin.flow.component.html.Div;
import com.vaadin.flow.component.progressbar.ProgressBar;
import com.vaadin.flow.component.textfield.TextField;
import com.vaadin.flow.data.binder.Binder;
import com.vaadin.flow.data.value.ValueChangeMode;
Expand All @@ -13,6 +15,7 @@
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import life.qbic.datamanager.views.general.dialog.AppDialog;
import life.qbic.datamanager.views.general.dialog.DialogBody;
import life.qbic.datamanager.views.general.dialog.DialogFooter;
Expand All @@ -21,10 +24,12 @@
import life.qbic.datamanager.views.general.dialog.InputValidation;
import life.qbic.datamanager.views.general.dialog.UserInput;
import life.qbic.datamanager.views.general.dialog.stepper.Step;
import life.qbic.datamanager.views.general.dialog.stepper.StepperDisplay;
import life.qbic.datamanager.views.general.dialog.stepper.StepperDialog;
import life.qbic.datamanager.views.general.dialog.stepper.StepperDialogFooter;
import life.qbic.datamanager.views.general.dialog.stepper.StepperDisplay;
import life.qbic.datamanager.views.general.icon.IconFactory;
import life.qbic.datamanager.views.notifications.MessageSourceNotificationFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
Expand All @@ -46,9 +51,13 @@ public class ComponentDemo extends Div {
public static final String HEADING_2 = "heading-2";
public static final String GAP_04 = "gap-04";
public static final String FLEX_VERTICAL = "flex-vertical";
public static final String NORMAL_BODY_TEXT = "normal-body-text";
Div title = new Div("Data Manager - Component Demo");
private final MessageSourceNotificationFactory messageFactory;

public ComponentDemo() {
@Autowired
public ComponentDemo(MessageSourceNotificationFactory messageSourceNotificationFactory) {
this.messageFactory = Objects.requireNonNull(messageSourceNotificationFactory);
title.addClassName("heading-1");
addClassNames("padding-left-right-07", "padding-top-bottom-04");
add(title);
Expand All @@ -59,6 +68,7 @@ public ComponentDemo() {
add(dialogShowCase(AppDialog.large(), "Large Dialog Type"));
add(dialogSectionShowCase());
add(stepperDialogShowCase(threeSteps(), "Three steps example"));
add(toastShowCase());
}

private static Div dialogSectionShowCase() {
Expand All @@ -84,7 +94,7 @@ private static Div headingShowcase() {
heading.addClassName("heading-" + i);
heading.setText("Heading " + i);
Div description = new Div();
description.addClassName("normal-body-text");
description.addClassName(NORMAL_BODY_TEXT);
description.setText("CSS class: %s".formatted(".heading-" + i));
container.add(heading, description);
}
Expand Down Expand Up @@ -155,11 +165,11 @@ private static Div stepperDialogShowCase(List<Step> steps, String dialogTitle) {

private static List<Step> threeSteps() {
List<Step> steps = new ArrayList<>();
for (int step= 0; step < 3; step++) {
for (int step = 0; step < 3; step++) {
int stepNumber = step + 1;
steps.add(new Step() {

final ExampleUserInput userInput = new ExampleUserInput("example step " + stepNumber );
final ExampleUserInput userInput = new ExampleUserInput("example step " + stepNumber);


@Override
Expand Down Expand Up @@ -218,10 +228,53 @@ private static Div dialogShowCase(AppDialog dialog, String dialogType) {
return content;
}

private Div toastShowCase() {
var title = new Div("Toast it!");
title.addClassName(HEADING_2);
var description = new Div(
"Let's see how toasts work and also how to use them when we want to indicate a background task to the user.");
description.addClassName(NORMAL_BODY_TEXT);
var content = new Div();
content.addClassNames(FLEX_VERTICAL, GAP_04);

content.add(title);
content.add(description);

var button = new Button("Show Toast");

content.add(button);

button.addClickListener(e ->
{
var progressBar = new ProgressBar();
progressBar.setIndeterminate(true);
var toast = messageFactory.pendingTaskToast("task.in-progress", new Object[]{"Doing something really heavy here"}, getLocale());
var succeededToast = messageFactory.toast("task.finished", new Object[]{"Heavy Task #1"}, getLocale());
toast.open();
var ui = UI.getCurrent();
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(5000);

} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}).thenRunAsync(() -> {
ui.access(() -> {
toast.close();
succeededToast.open();
}
);
});
});
content.add(button);
return content;
}

private static class BodyFontStyles {

static String[] fontStyles = new String[]{
"normal-body-text",
NORMAL_BODY_TEXT,
"small-body-text",
"extra-small-body-text",
"field-label-text",
Expand All @@ -238,7 +291,8 @@ private static class ExampleUserInput extends Div implements UserInput {
Binder<StringBean> binder;

ExampleUserInput(String prefill) {
var dialogSection = DialogSection.with("User Input Validation", "Try correct and incorrect input values in the following field.");
var dialogSection = DialogSection.with("User Input Validation",
"Try correct and incorrect input values in the following field.");
originalValue = prefill;
var textField = new TextField();
textField.setLabel("Correct input is 'Riddikulus'");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.vaadin.flow.component.Component;
import com.vaadin.flow.component.Html;
import com.vaadin.flow.component.html.Span;
import com.vaadin.flow.component.progressbar.ProgressBar;
import com.vaadin.flow.router.RouteParameters;
import com.vaadin.flow.spring.annotation.SpringComponent;
import java.time.Duration;
Expand All @@ -17,8 +18,8 @@
import org.springframework.context.NoSuchMessageException;

/**
* Notifications created by this factory can be shown to the user.
* There are multiply types of notifications.
* Notifications created by this factory can be shown to the user. There are multiply types of
* notifications.
* <ul>
* <li>Toast notification</li>
* <li>Notification dialog</li>
Expand All @@ -33,8 +34,8 @@
@SpringComponent
public class MessageSourceNotificationFactory {

private static final Logger log = logger(MessageSourceNotificationFactory.class);
public static final Object[] EMPTY_PARAMETERS = new Object[]{};
private static final Logger log = logger(MessageSourceNotificationFactory.class);
private static final String DEFAULT_CONFIRM_TEXT = "Okay";
private static final NotificationLevel DEFAULT_LEVEL = NotificationLevel.INFO;
private static final MessageType DEFAULT_MESSAGE_TYPE = MessageType.HTML;
Expand Down Expand Up @@ -109,6 +110,34 @@ public Toast routingToast(String key, Object[] messageArgs, Object[] routeArgs,
return toast.withRouting(linkText, navigationTarget, routeParameters);
}

/**
* Creates a toast that indicates a pending task. The display duration is set to
* {@link Duration#ZERO}, since it is the client's job to close the toast explicitly after the
* pending task has finished.
* <p>
* The following message keys have to be present:
* <ul>
* <li>{@code <key>.message.type}
* <li>{@code <key>.message.text}
* <li>{@code <key>.routing.link.text}
* </ul>
* <p>
* For more information please see toast-notifications.properties
*
* @param key the key for the messages
* @param messageArgs the parameters shown in the message
* @param locale the locale for which to load the message
* @return a Toast with loaded content
* @see #toast(String, Object[], Locale)
*/
public Toast pendingTaskToast(String key, Object[] messageArgs, Locale locale) {
var toast = toast(key, messageArgs, locale);
var progressBar = new ProgressBar();
progressBar.setIndeterminate(true);
toast.setDuration(Duration.ZERO);
return toast.add(progressBar);
}

/**
* Creates a dialog notification with the contents found for the message key. This method produces
* a notification dialog with the link text read from the message properties file.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import life.qbic.datamanager.views.general.ComponentFunctions;

/**
Expand All @@ -32,10 +33,9 @@
*/
public final class Toast extends Notification {

private static final Position DEFAULT_POSITION = Position.BOTTOM_START;
static final boolean DEFAULT_CLOSE_ON_NAVIGATION = true;
static final Duration DEFAULT_OPEN_DURATION = Duration.ofSeconds(5);

private static final Position DEFAULT_POSITION = Position.BOTTOM_START;
private final List<Registration> closeOnNavigationListeners = new ArrayList<>();
private final Button closeButton;

Expand Down Expand Up @@ -67,6 +67,22 @@ private static Button buttonClosing(Toast toast) {
return button;
}

/**
* Creates a matching toast content containing routing components with the correct css classes.
*
* @param content
* @param routingComponent
* @return
*/
private static Component createRoutingContent(Component content, Component routingComponent) {
var container = new Div();
container.addClassName("routing-container");
content.addClassName("routing-content");
routingComponent.addClassName("routing-link");
container.add(content, routingComponent);
return container;
}

/**
* Sets whether toasts are closed after navigation. By default, Toasts do not stay open after
* navigation.
Expand Down Expand Up @@ -104,7 +120,6 @@ public void setDuration(Duration duration) {
super.setDuration((int) duration.toMillis());
}


/**
* Expands the toast and includes a link to a specific route target.
*
Expand Down Expand Up @@ -143,23 +158,6 @@ private void refresh() {
add(this.content, this.closeButton);
}


/**
* Creates a matching toast content containing routing components with the correct css classes.
*
* @param content
* @param routingComponent
* @return
*/
private static Component createRoutingContent(Component content, Component routingComponent) {
var container = new Div();
container.addClassName("routing-container");
content.addClassName("routing-content");
routingComponent.addClassName("routing-link");
container.add(content, routingComponent);
return container;
}

/**
* Creates a routing component with correct css classes and layout.
*
Expand All @@ -178,4 +176,28 @@ private Component createRoutingComponent(String text,
routerLink.add(button);
return routerLink;
}

/**
* Adds a component to the {@link Toast}. If content already exists, the existing component is
* taken and wrapped together with the new component in a {@link Div}, without extra formatting.
* <p>
* If no content yet exists, the passed component is taken.
*
* @param component the component to add to the toast
* @return the toast
* @since 1.8.0
*/
Toast add(Component component) {
Objects.requireNonNull(component);
if (nonNull(this.content)) {
var copy = this.content;
var newContent = new Div();
newContent.add(copy, component);
this.content = newContent;
} else {
this.content = component;
}
refresh();
return this;
}
}
Loading

0 comments on commit 6dfe9dd

Please sign in to comment.