diff --git a/datamanager-app/front-end-components.md b/datamanager-app/front-end-components.md index 6faad7e531..287919ea4b 100644 --- a/datamanager-app/front-end-components.md +++ b/datamanager-app/front-end-components.md @@ -136,5 +136,127 @@ classDiagram ``` +## Alert dialog + +```mermaid +classDiagram + note for Component "Vaadin Component" + AlertDialog <-- AppDialog + AppDialog --> DialogHeader + AppDialog --> DialogBody + AppDialog --> DialogFooter + AppDialog --|> Dialog + + class AlertDialog { + <> + +alert(Component parent) Builder + +danger(Component parent, String title, String message, DialogAction onConfirm) AlertDialog + +open() + +close() + +dialog() AppDialog + } + + class Builder { + +intent(Intent intent) Builder + +danger() Builder + +warning() Builder + +error() Builder + +info() Builder + +title(String title) Builder + +message(String message) Builder + +confirmButton(String label, DialogAction action) Builder + +cancelButton(String label, DialogAction action) Builder + +build() AlertDialog + } + + class AppDialog { + +small() AppDialog + +registerConfirmAction(DialogAction action) + +registerCancelAction(DialogAction action) + +open() + +close() + } + + class DialogHeader { + +withIcon(AppDialog dialog, String title, Icon icon) + } + + class DialogBody { + +withoutUserInput(AppDialog dialog, Component component) + } + + class DialogFooter { + +withDangerousConfirm(AppDialog dialog, String cancelText, String confirmText) + +with(AppDialog dialog, String cancelText, String confirmText) + +withConfirmOnly(AppDialog dialog, String confirmText) + } + + class DialogAction { + <> + +execute() + } +``` + +### When to use + +| Need | Pattern | +|---|---| +| Destructive action confirmation | `AlertDialog.danger(parent, title, message, onConfirm).open()` | +| Error requiring acknowledgment | `AlertDialog.alert(parent).error().title(...).message(...).confirmButton("OK", () -> {}).build().open()` | +| Error with redirect + cancel | `AlertDialog.alert(parent).error().confirmButton("Go...", nav).cancelButton("Cancel", close).build().open()` | +| Cancel / discard changes | `CancelConfirmationDialogFactory.cancelConfirmationDialog(action, key, locale).open()` | +| Success / info / transient feedback | `MessageSourceNotificationFactory.toast(...)` (unchanged) | +| Pending task with progress bar | `MessageSourceNotificationFactory.pendingTaskToast(...)` (unchanged) | + +### Code examples + +**Destructive confirmation (most common):** +```java +AlertDialog.danger(this, + "Samples within batch will be deleted", + "Deleting this Batch will also delete the samples contained within. Proceed?", + () -> deleteBatch(batchId)).open(); +``` + +**Error dialog with single button:** +```java +AlertDialog.alert(this) + .error() + .title("Cannot edit variables") + .message("Editing experimental variables is only possible if samples are not registered.") + .confirmButton("Okay", () -> {}) + .build() + .open(); +``` + +**Warning with cancel:** +```java +AlertDialog.alert(this) + .warning() + .title("Discard changes?") + .message("By aborting, you will lose all entered information.") + .confirmButton("Discard", () -> discard()) + .cancelButton("Continue Editing", () -> {}) // close dialog + .build() + .open(); +``` + +### Deprecated: NotificationDialog + +`NotificationDialog` has been removed and replaced with `AlertDialog`. The following classes were deleted: + +- `AccessTokenDeletionConfirmationNotification` +- `BatchDeletionConfirmationNotification` +- `MeasurementDeletionConfirmationNotification` +- `QCItemDeletionConfirmationNotification` +- `PurchaseItemDeletionConfirmationNotification` +- `ProjectUserRemovalConfirmationNotification` +- `ExistingGroupsPreventVariableEdit` +- `ExistingSamplesPreventVariableEdit` +- `ExistingSamplesPreventSampleOriginEdit` +- `ExistingSamplesPreventGroupEdit` + +If any code still references `NotificationDialog`, it will throw `UnsupportedOperationException` at runtime. Use the patterns above instead. + diff --git a/datamanager-app/src/main/bundles/dev.bundle b/datamanager-app/src/main/bundles/dev.bundle index ce534ad917..9632ad3661 100644 Binary files a/datamanager-app/src/main/bundles/dev.bundle and b/datamanager-app/src/main/bundles/dev.bundle differ diff --git a/datamanager-app/src/main/java/life/qbic/datamanager/exceptionhandling/UiExceptionHandler.java b/datamanager-app/src/main/java/life/qbic/datamanager/exceptionhandling/UiExceptionHandler.java index 1de0d7e423..2917f7af3f 100644 --- a/datamanager-app/src/main/java/life/qbic/datamanager/exceptionhandling/UiExceptionHandler.java +++ b/datamanager-app/src/main/java/life/qbic/datamanager/exceptionhandling/UiExceptionHandler.java @@ -5,13 +5,12 @@ import static life.qbic.logging.service.LoggerFactory.logger; import com.vaadin.flow.component.UI; -import com.vaadin.flow.component.html.Span; import com.vaadin.flow.server.ErrorEvent; import java.net.InetAddress; import java.net.UnknownHostException; import life.qbic.application.commons.ApplicationException; import life.qbic.datamanager.exceptionhandling.ErrorMessageTranslationService.UserFriendlyErrorMessage; -import life.qbic.datamanager.views.notifications.NotificationDialog; +import life.qbic.datamanager.views.general.dialog.AlertDialog; import life.qbic.logging.api.Logger; import life.qbic.projectmanagement.application.associated_dataset.AssociatedDatasetServiceException; import org.springframework.beans.factory.annotation.Autowired; @@ -126,9 +125,12 @@ private boolean isUiReady(UI ui) { } private void showErrorDialog(UserFriendlyErrorMessage userFriendlyError) { - NotificationDialog.errorDialog() - .withTitle(userFriendlyError.title()) - .withContent(new Span(userFriendlyError.message())) + AlertDialog.alert(UI.getCurrent()) + .error() + .title(userFriendlyError.title()) + .message(userFriendlyError.message()) + .confirmButton("Got it", () -> {}) + .build() .open(); } } diff --git a/datamanager-app/src/main/java/life/qbic/datamanager/views/account/AccessTokenDeletionConfirmationNotification.java b/datamanager-app/src/main/java/life/qbic/datamanager/views/account/AccessTokenDeletionConfirmationNotification.java deleted file mode 100644 index b8dee23bb5..0000000000 --- a/datamanager-app/src/main/java/life/qbic/datamanager/views/account/AccessTokenDeletionConfirmationNotification.java +++ /dev/null @@ -1,24 +0,0 @@ -package life.qbic.datamanager.views.account; - -import com.vaadin.flow.component.html.Span; -import life.qbic.datamanager.views.notifications.NotificationDialog; -import life.qbic.datamanager.views.notifications.NotificationLevel; - -/** - * Warns the user that the personal access token will be deleted and cannot be used - *

- * This dialog is to be shown when PAT delection is triggered by the user. - */ -public class AccessTokenDeletionConfirmationNotification extends NotificationDialog { - - public AccessTokenDeletionConfirmationNotification() { - super(NotificationLevel.WARNING); - withTitle("Personal Access Token will be deleted"); - withContent(new Span( - "Deleting this Personal Access Token will make it unusable. Proceed?")); - setCancelable(true); - setConfirmText("Delete Token"); - } - - -} diff --git a/datamanager-app/src/main/java/life/qbic/datamanager/views/account/PersonalAccessTokenMain.java b/datamanager-app/src/main/java/life/qbic/datamanager/views/account/PersonalAccessTokenMain.java index a0ea248599..8f4ff56bd2 100644 --- a/datamanager-app/src/main/java/life/qbic/datamanager/views/account/PersonalAccessTokenMain.java +++ b/datamanager-app/src/main/java/life/qbic/datamanager/views/account/PersonalAccessTokenMain.java @@ -18,6 +18,7 @@ import life.qbic.datamanager.views.account.PersonalAccessTokenComponent.DeleteTokenEvent; import life.qbic.datamanager.views.account.PersonalAccessTokenComponent.PersonalAccessTokenFrontendBean; import life.qbic.datamanager.views.general.Main; +import life.qbic.datamanager.views.general.dialog.AlertDialog; import life.qbic.datamanager.views.notifications.MessageSourceNotificationFactory; import life.qbic.datamanager.views.notifications.Toast; import life.qbic.identity.api.PersonalAccessToken; @@ -74,18 +75,18 @@ public PersonalAccessTokenMain(PersonalAccessTokenService personalAccessTokenSer } private void onDeleteTokenClicked(DeleteTokenEvent deleteTokenEvent) { - AccessTokenDeletionConfirmationNotification tokenDeletionConfirmationNotification = new AccessTokenDeletionConfirmationNotification(); - tokenDeletionConfirmationNotification.open(); - tokenDeletionConfirmationNotification.addConfirmListener(event -> { - var userId = userIdTranslator.translateToUserId( - SecurityContextHolder.getContext().getAuthentication()) - .orElseThrow(); - personalAccessTokenService.delete(deleteTokenEvent.tokenId(), userId); - loadGeneratedPersonalAccessTokens(); - tokenDeletionConfirmationNotification.close(); - }); - tokenDeletionConfirmationNotification.addCancelListener( - event -> tokenDeletionConfirmationNotification.close()); + AlertDialog.danger(this, + "Personal Access Token will be deleted", + "Deleting this Personal Access Token will make it unusable. Proceed?", + "Delete token", + "Keep token", + () -> { + var userId = userIdTranslator.translateToUserId( + SecurityContextHolder.getContext().getAuthentication()) + .orElseThrow(); + personalAccessTokenService.delete(deleteTokenEvent.tokenId(), userId); + loadGeneratedPersonalAccessTokens(); + }).open(); } private void onAddTokenClicked(AddTokenEvent addTokenEvent) { diff --git a/datamanager-app/src/main/java/life/qbic/datamanager/views/general/dialog/AlertDialog.java b/datamanager-app/src/main/java/life/qbic/datamanager/views/general/dialog/AlertDialog.java index 580dbff41c..e90dbf7bf6 100644 --- a/datamanager-app/src/main/java/life/qbic/datamanager/views/general/dialog/AlertDialog.java +++ b/datamanager-app/src/main/java/life/qbic/datamanager/views/general/dialog/AlertDialog.java @@ -30,15 +30,16 @@ *

Two entry points:

*
{@code
  * // Shortcut for the most common case: danger-intent confirm dialog
- * AlertDialog.danger(parent, "Remove dataset?", "This will disconnect…", onRemove).open();
+ * AlertDialog.danger(parent, "Remove dataset?", "This will disconnect…",
+ *     "Disconnect dataset", "Keep connection", onRemove).open();
  *
  * // Fluent builder for full control
  * AlertDialog.alert(parent)
  *     .danger()
  *     .title("Remove dataset?")
  *     .message("This will disconnect…")
- *     .confirmButton("Remove", onRemove)
- *     .cancelButton("Cancel", onCancel)
+ *     .confirmButton("Disconnect dataset", onRemove)
+ *     .cancelButton("Keep connection", onCancel)
  *     .build().open();
  * }
* @@ -179,11 +180,11 @@ public AlertDialog build() { messageDiv.add(new Span(message)); DialogBody.withoutUserInput(dialog, messageDiv); - // Footer — danger intent gets a red confirm button via the existing + // Footer — DANGER and WARNING intents get a red confirm button via the // withDangerousConfirm helper; other intents use the default primary // style. If no cancel action is set, the builder skips the cancel // button (single-action confirmation). - if (intent == Intent.DANGER && cancelLabel != null && cancelAction != null) { + if ((intent == Intent.DANGER || intent == Intent.WARNING) && cancelLabel != null && cancelAction != null) { DialogFooter.withDangerousConfirm(dialog, cancelLabel, confirmLabel); } else if (cancelLabel != null && cancelAction != null) { DialogFooter.with(dialog, cancelLabel, confirmLabel); @@ -282,24 +283,40 @@ public static Builder alert(Component parent) { * confirm button, a cancel button labelled "Cancel" that simply * closes the dialog, and a body message with the given text. * - *

The cancel action only closes the dialog (no external side-effect). - * If the caller needs a custom cancel handler, use {@link #alert(Component)} - * and {@code .cancelButton(...)} explicitly.

+ *

The {@code confirmLabel} should describe the destructive action + * in a concise, action-oriented verb (e.g. "Remove user", + * "Disconnect dataset", "Delete offer") so the user knows exactly + * what clicking it will do. The {@code cancelLabel} should describe + * the outcome of dismissing the confirm (e.g. "Keep user", + * "Keep connection", "Keep offer") so both buttons clearly state + * their respective outcomes. This avoids generic single-word labels + * and minimises cognitive load. + * The cancel action only closes the dialog (no external side-effect). + * If the caller needs a custom cancel handler, use + * {@link #alert(Component)} and {@code .cancelButton(...)} explicitly.

* *

Usage:

*
{@code
    *   AlertDialog.danger(this,
    *       "Remove dataset connection?",
    *       "This will disconnect the dataset from the project.",
+   *       "Disconnect dataset",
+   *       "Keep connection",
    *       () -> performRemove())
    *       .open();
    * }
* - * @param parent the component that owns this dialog (for UI tree - * attachment) - * @param title the header text next to the icon - * @param message the body description - * @param onConfirm the action invoked when the user confirms + * @param parent the component that owns this dialog (for UI tree + * attachment) + * @param title the header text next to the icon + * @param message the body description + * @param confirmLabel a concise, action-oriented label for the confirm + * button (e.g. "Remove user", "Delete offer", + * "Disconnect dataset") + * @param cancelLabel a label describing the outcome of cancelling + * (e.g. "Keep user", "Keep offer", + * "Keep connection") + * @param onConfirm the action invoked when the user confirms * @return a ready-to-{@code open()} AlertDialog * @throws NullPointerException if any argument is null */ @@ -307,13 +324,15 @@ public static AlertDialog danger( Component parent, String title, String message, + String confirmLabel, + String cancelLabel, DialogAction onConfirm) { return alert(parent) .danger() .title(title) .message(message) - .confirmButton("Confirm", onConfirm) - .cancelButton("Cancel", () -> { /* dismiss only */ }) + .confirmButton(confirmLabel, onConfirm) + .cancelButton(cancelLabel, () -> { /* dismiss only */ }) .build(); } } diff --git a/datamanager-app/src/main/java/life/qbic/datamanager/views/general/dialog/AppDialog.java b/datamanager-app/src/main/java/life/qbic/datamanager/views/general/dialog/AppDialog.java index c2282c9346..abd375bc82 100644 --- a/datamanager-app/src/main/java/life/qbic/datamanager/views/general/dialog/AppDialog.java +++ b/datamanager-app/src/main/java/life/qbic/datamanager/views/general/dialog/AppDialog.java @@ -103,8 +103,8 @@ private static AppDialog createConfirmDialog(DialogAction onConfirmAction) { IconFactory.warningIcon()); DialogBody.withoutUserInput(confirmDialog, new Div( "By aborting the editing process and closing the dialog, you will lose all information entered.")); - life.qbic.datamanager.views.general.dialog.DialogFooter.with(confirmDialog, "Continue editing", - "Discard changes"); + life.qbic.datamanager.views.general.dialog.DialogFooter.withDangerousConfirm(confirmDialog, + "Keep editing", "Discard changes"); confirmDialog.registerConfirmAction(() -> { confirmDialog.close(); onConfirmAction.execute(); diff --git a/datamanager-app/src/main/java/life/qbic/datamanager/views/navigation/ProjectSideNavigationComponent.java b/datamanager-app/src/main/java/life/qbic/datamanager/views/navigation/ProjectSideNavigationComponent.java index 4c97837e27..e6c0527b96 100644 --- a/datamanager-app/src/main/java/life/qbic/datamanager/views/navigation/ProjectSideNavigationComponent.java +++ b/datamanager-app/src/main/java/life/qbic/datamanager/views/navigation/ProjectSideNavigationComponent.java @@ -32,9 +32,8 @@ import life.qbic.datamanager.security.UserPermissions; import life.qbic.datamanager.views.AppRoutes.ProjectRoutes; import life.qbic.datamanager.views.Context; -import life.qbic.datamanager.views.notifications.CancelConfirmationDialogFactory; +import life.qbic.datamanager.views.general.dialog.AlertDialog; import life.qbic.datamanager.views.notifications.MessageSourceNotificationFactory; -import life.qbic.datamanager.views.notifications.NotificationDialog; import life.qbic.datamanager.views.notifications.Toast; import life.qbic.datamanager.views.projects.overview.ProjectOverviewMain; import life.qbic.datamanager.views.projects.project.ProjectMainLayout; @@ -79,7 +78,6 @@ public class ProjectSideNavigationComponent extends Div implements private final transient ExperimentInformationService experimentInformationService; private final transient AddExperimentToProjectService addExperimentToProjectService; private final transient UserPermissions userPermissions; - private final transient CancelConfirmationDialogFactory cancelConfirmationDialogFactory; private final transient MessageSourceNotificationFactory messageSourceNotificationFactory; private final transient TerminologyService terminologyService; private final transient SpeciesLookupService speciesLookupService; @@ -92,7 +90,6 @@ public ProjectSideNavigationComponent( UserPermissions userPermissions, SpeciesLookupService speciesLookupService, TerminologyService terminologyService, - CancelConfirmationDialogFactory cancelConfirmationDialogFactory, MessageSourceNotificationFactory messageSourceNotificationFactory) { content = new Div(); requireNonNull(projectInformationService); @@ -100,8 +97,6 @@ public ProjectSideNavigationComponent( requireNonNull(addExperimentToProjectService); this.messageSourceNotificationFactory = requireNonNull(messageSourceNotificationFactory, "messageSourceNotificationFactory must not be null"); - this.cancelConfirmationDialogFactory = requireNonNull(cancelConfirmationDialogFactory, - "cancelConfirmationDialogFactory must not be null"); this.speciesLookupService = speciesLookupService; this.addExperimentToProjectService = addExperimentToProjectService; this.userPermissions = requireNonNull(userPermissions, "userPermissions must not be null"); @@ -340,12 +335,14 @@ private void showAddExperimentDialog() { } private void showCancelConfirmationDialog(AddExperimentDialog creationDialog) { - NotificationDialog confirmationDialog = cancelConfirmationDialogFactory.cancelConfirmationDialog( - it -> creationDialog.close(), - "experiment.create", - getLocale() - ); - confirmationDialog.open(); + AlertDialog.alert(this) + .warning() + .title("Discard changes?") + .message("By aborting the editing process and closing the dialog, you will lose all information entered.") + .confirmButton("Discard changes", creationDialog::close) + .cancelButton("Keep editing", () -> {}) + .build() + .open(); } private void onExperimentAddEvent(ExperimentAddEvent event) { diff --git a/datamanager-app/src/main/java/life/qbic/datamanager/views/notifications/CancelConfirmationDialogFactory.java b/datamanager-app/src/main/java/life/qbic/datamanager/views/notifications/CancelConfirmationDialogFactory.java deleted file mode 100644 index 0bd3895cf8..0000000000 --- a/datamanager-app/src/main/java/life/qbic/datamanager/views/notifications/CancelConfirmationDialogFactory.java +++ /dev/null @@ -1,113 +0,0 @@ -package life.qbic.datamanager.views.notifications; - -import com.vaadin.flow.component.Html; -import com.vaadin.flow.component.button.Button; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog.ConfirmEvent; -import com.vaadin.flow.component.html.Span; -import java.util.Locale; -import java.util.function.Consumer; -import life.qbic.logging.api.Logger; -import life.qbic.logging.service.LoggerFactory; -import org.springframework.context.MessageSource; -import org.springframework.context.NoSuchMessageException; -import org.springframework.stereotype.Component; - -@Component -public class CancelConfirmationDialogFactory { - - private static final MessageType DEFAULT_MESSAGE_TYPE = MessageType.HTML; //set to html as text works with it too - private static final String DEFAULT_TITLE = "Discard Changes?"; - private static final String DEFAULT_CONTENT = "By aborting the editing process and closing the dialog, you will lose all information entered."; - private static final String DEFAULT_CONFIRM_TEXT = "Discard Changes"; - private static final Object[] EMPTY_PARAMETERS = new Object[]{}; - private static final Logger log = LoggerFactory.logger(CancelConfirmationDialogFactory.class); - private final MessageSource messageSource; - - - public CancelConfirmationDialogFactory(MessageSource messageSource) { - this.messageSource = messageSource; - } - - private static com.vaadin.flow.component.Component createContentComponent(MessageType contentType, - String contentText) { - return switch (contentType) { - case HTML -> new Html("
%s
".formatted(contentText)); - case TEXT -> new Span(contentText); - }; - } - - /** - * Creates an instance of an {@link NotificationDialog} with title and message based on the - * provided key. - * - * @param key The message key to determine the content and title of the {@link NotificationDialog}. - * @param locale - * @return the notification dialog - * @since - */ - public NotificationDialog cancelConfirmationDialog(String key, Locale locale) { - return buildDialog(key, locale); - } - - public NotificationDialog cancelConfirmationDialog(Consumer onCancelConfirmed, - String key, Locale locale) { - var confirmCancelDialog = buildDialog(key, locale); - confirmCancelDialog.addCancelListener(event -> event.getSource().close()); - - confirmCancelDialog.addConfirmListener(event -> { - event.getSource().close(); - onCancelConfirmed.accept(event); - }); - return confirmCancelDialog; - } - - private NotificationDialog buildDialog(String key, Locale locale) { - String title = parseTitle(key, locale); - MessageType contentType = parseMessageType(key, locale); - String contentText = parseMessageText(key, locale); - String confirmText = parseConfirmText(key, locale); - var content = createContentComponent(contentType, contentText); - - NotificationDialog confirmCancelDialog = NotificationDialog.warningDialog() - .withTitle(title) - .withContent(content); - confirmCancelDialog.setCancelable(true); - confirmCancelDialog.setCancelText("Continue Editing"); - Button redButton = new Button(confirmText); - redButton.addClassName("danger"); - confirmCancelDialog.setConfirmButton(redButton); - return confirmCancelDialog; - } - - private String parseConfirmText(String key, Locale locale) { - return messageSource.getMessage("%s.cancel-confirmation.confirm-text".formatted(key), - new Object[]{}, DEFAULT_CONFIRM_TEXT, locale); - } - - private String parseMessageText(String key, Locale locale) { - return messageSource.getMessage("%s.cancel-confirmation.message.text".formatted(key), - new Object[]{}, DEFAULT_CONTENT, locale); - } - - private String parseTitle(String key, Locale locale) { - return messageSource.getMessage("%s.cancel-confirmation.title".formatted(key), - new Object[]{}, DEFAULT_TITLE, locale); - } - - private MessageType parseMessageType(String key, Locale locale) { - try { - String messageType = messageSource.getMessage( - "%s.cancel-confirmation.message.type".formatted(key), - EMPTY_PARAMETERS, locale).strip().toUpperCase(); - return MessageType.valueOf(messageType); - } catch (NoSuchMessageException e) { - log.warn("No message type specified for %s.".formatted(key)); - return DEFAULT_MESSAGE_TYPE; - } - } - - private enum MessageType { - HTML, - TEXT - } -} diff --git a/datamanager-app/src/main/java/life/qbic/datamanager/views/notifications/MessageSourceNotificationFactory.java b/datamanager-app/src/main/java/life/qbic/datamanager/views/notifications/MessageSourceNotificationFactory.java index e29baf3e49..29a8bddc19 100644 --- a/datamanager-app/src/main/java/life/qbic/datamanager/views/notifications/MessageSourceNotificationFactory.java +++ b/datamanager-app/src/main/java/life/qbic/datamanager/views/notifications/MessageSourceNotificationFactory.java @@ -36,7 +36,6 @@ public class MessageSourceNotificationFactory { 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; private final MessageSource messageSource; @@ -138,46 +137,6 @@ public Toast pendingTaskToast(String key, Object[] messageArgs, Locale locale) { 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. - * - *

- * The following message keys have to be present: - *

    - *
  • {@code .title} - the title; optional - *
  • {@code .level} - the level; mandatory - *
  • {@code .message.type} - the type (text or html); mandatory - *
  • {@code .message.text} - the text; mandatory - *
  • {@code .confirm-text} - the text of the confirm button; optional - *
- *

- * For more information please see dialog-notifications.properties - * - * @param key the key for the messages - * @param parameters parameters to use in the message - * @param locale the locale for which to load the message - * @return a notification dialog with loaded content - */ - public NotificationDialog dialog(String key, Object[] parameters, Locale locale) { - MessageType type = parseMessageType(key, locale); - String messageText = parseMessage(key, parameters, locale); - Component content = switch (type) { - case HTML -> new Html("

%s
".formatted(messageText)); - case TEXT -> new Span(messageText); - }; - - NotificationLevel level = parseLevel(key, locale); - NotificationDialog notificationDialog = new NotificationDialog(level) - .withContent(content); - parseTitle(key, locale).ifPresent(notificationDialog::withTitle); - parseConfirmText(key, locale).ifPresentOrElse( - notificationDialog::setConfirmText, - () -> notificationDialog.setConfirmText(DEFAULT_CONFIRM_TEXT)); - - return notificationDialog; - } - private NotificationLevel parseLevel(String key, Locale locale) { String levelProperty; try { @@ -222,16 +181,6 @@ private MessageType parseMessageType(String key, Locale locale) { } - private Optional parseTitle(String key, Locale locale) { - try { - return Optional.of(messageSource.getMessage("%s.title".formatted(key), - EMPTY_PARAMETERS, locale).strip()); - } catch (NoSuchMessageException e) { - log.warn("No title specified for %s.title".formatted(key)); - return Optional.empty(); - } - } - private Optional parseDuration(String key, Locale locale) { String durationProperty = messageSource.getMessage("%s.duration".formatted(key), EMPTY_PARAMETERS, null, locale); @@ -257,11 +206,6 @@ private String parseLinkText(String key, Object[] routeArgs, Locale locale) { return linkText; } - private Optional parseConfirmText(String key, Locale locale) { - return Optional.ofNullable(messageSource.getMessage("%s.confirm-text".formatted(key), - new Object[]{}, null, locale)); - } - private enum MessageType { HTML, TEXT diff --git a/datamanager-app/src/main/java/life/qbic/datamanager/views/notifications/NotificationDialog.java b/datamanager-app/src/main/java/life/qbic/datamanager/views/notifications/NotificationDialog.java deleted file mode 100644 index 8f3a9b1e3b..0000000000 --- a/datamanager-app/src/main/java/life/qbic/datamanager/views/notifications/NotificationDialog.java +++ /dev/null @@ -1,200 +0,0 @@ -package life.qbic.datamanager.views.notifications; - -import static java.util.Objects.requireNonNull; - -import com.vaadin.flow.component.Component; -import com.vaadin.flow.component.confirmdialog.ConfirmDialog; -import com.vaadin.flow.component.html.Div; -import com.vaadin.flow.component.html.H2; -import com.vaadin.flow.component.html.Span; -import com.vaadin.flow.component.icon.Icon; -import com.vaadin.flow.component.icon.VaadinIcon; -import com.vaadin.flow.dom.Style.Display; -import com.vaadin.flow.router.BeforeLeaveEvent; -import com.vaadin.flow.router.BeforeLeaveObserver; - -/** - * A dialog notifying the user of some event. - *

- * By default, this dialog comes with an icon and a text in the header. You can modify the header - * text with {@link #withTitle}. When setting the icon with {@link #withHeaderIcon}, you can modify - * the color of the icon by assigning the following css classes: - *

    - *
  • success-icon
  • - *
  • warning-icon
  • - *
  • error-icon
  • - *
  • info-icon
  • - *
- * The dialog itself is assigned a corresponding CSS class - *
    - *
  • success-dialog
  • - *
  • warning-dialog
  • - *
  • error-dialog
  • - *
  • info-dialog
  • - *
- */ -public class NotificationDialog extends ConfirmDialog implements BeforeLeaveObserver { - - private final H2 title; - private final NotificationLevel level; - protected final Div layout; - private Icon headerIcon; - protected Component content; - - protected NotificationDialog(NotificationLevel level) { - addClassName("notification-dialog"); - addClassName(switch (level) { - case SUCCESS -> "success-dialog"; - case WARNING -> "warning-dialog"; - case ERROR -> "error-dialog"; - case INFO -> "info-dialog"; - }); - this.level = requireNonNull(level, "level must not be null"); - - var defaultTitle = switch (level) { - case SUCCESS -> "Success"; - case WARNING -> "Warning"; - case ERROR -> "Error"; - case INFO -> "Please note"; - }; - title = new H2(defaultTitle); - title.addClassName("title"); - withHeaderIcon(typeBasedHeaderIcon(this.level)); - updateHeader(); - - layout = new Div(); - layout.addClassName("content"); - this.content = new Div(); - layout.add(this.content); - setText(layout); - setConfirmText("Okay"); - } - - protected static Icon typeBasedHeaderIcon(NotificationLevel notificationLevel) { - var iconCssClass = switch (notificationLevel) { - case SUCCESS -> "success-icon"; - case WARNING -> "warning-icon"; - case ERROR -> "error-icon"; - case INFO -> "info-icon"; - }; - var icon = switch (notificationLevel) { - case SUCCESS -> VaadinIcon.CHECK.create(); - case WARNING -> VaadinIcon.WARNING.create(); - case ERROR -> VaadinIcon.CLOSE_CIRCLE.create(); - case INFO -> VaadinIcon.INFO_CIRCLE.create(); - }; - icon.addClassName(iconCssClass); - return icon; - } - - /** - * Creates a new notification dialog - * - * @return a notification dialog showing a success notification - */ - public static NotificationDialog successDialog() { - return new NotificationDialog(NotificationLevel.SUCCESS); - } - - /** - * Creates a new notification dialog - * - * @return a notification dialog showing a warning notification - */ - public static NotificationDialog warningDialog() { - return new NotificationDialog(NotificationLevel.WARNING); - } - - /** - * Creates a new notification dialog - * - * @return a notification dialog showing an error notification - */ - public static NotificationDialog errorDialog() { - return new NotificationDialog(NotificationLevel.ERROR); - } - - /** - * Creates a new notification dialog - * - * @return a notification dialog showing an info notification - */ - public static NotificationDialog infoDialog() { - return new NotificationDialog(NotificationLevel.INFO); - } - - private void updateHeader() { - setHeader(new Span(headerIcon, title)); - } - - /** - * Changes the header icon to the icon specified. - * - * @param icon the icon to display in the header - * @param - * @return a modified notification dialog - */ - public T withHeaderIcon(Icon icon) { - this.headerIcon = icon; - updateHeader(); - return (T) this; - } - - /** - * Changes the title of the dialog. Does not touch the icon. - * - * @param text the title of the dialog - * @param - * @return a dialog with the provided title - */ - public T withTitle(String text) { - title.setText(text); - updateHeader(); - return (T) this; - } - - /** - * Sets the content of the dialog. - *

- * The content can be any {@link Component}. Previous content is removed from the dialog when - * calling this method. The content provided must not be null but can be any empty component. - * - * @param content the new content of the dialog - * @param - * @return the modified dialog - */ - public T withContent(Component content) { - if (this.content != null) { - this.content.removeFromParent(); - } - this.content = requireNonNull(content, "content must not be null"); - layout.removeAll(); - layout.add(this.content); - return (T) this; - } - - /** - * Sets the content of the dialog. - *

- * The content can be any number of {@link Component}s. At least one component is required. - *

- * Previous content is removed from the dialog when calling this method. - * - * @param content the new content of the dialog - * @param - * @return the modified dialog - */ - public T withContent(Component... content) { - if (content.length <= 0) { - throw new IllegalArgumentException("Content must have at least one element"); - } - Div hiddenCollectionDiv = new Div(content); - hiddenCollectionDiv.getStyle().setDisplay(Display.CONTENTS); - return withContent(hiddenCollectionDiv); - } - - @Override - public void beforeLeave(BeforeLeaveEvent event) { - this.close(); - } -} diff --git a/datamanager-app/src/main/java/life/qbic/datamanager/views/projects/create/AddProjectDialog.java b/datamanager-app/src/main/java/life/qbic/datamanager/views/projects/create/AddProjectDialog.java index ccad715077..e64060a48a 100644 --- a/datamanager-app/src/main/java/life/qbic/datamanager/views/projects/create/AddProjectDialog.java +++ b/datamanager-app/src/main/java/life/qbic/datamanager/views/projects/create/AddProjectDialog.java @@ -23,9 +23,8 @@ import life.qbic.datamanager.views.general.QbicDialog; import life.qbic.datamanager.views.general.Stepper; import life.qbic.datamanager.views.general.Stepper.StepIndicator; +import life.qbic.datamanager.views.general.dialog.AlertDialog; import life.qbic.datamanager.views.general.funding.FundingEntry; -import life.qbic.datamanager.views.notifications.CancelConfirmationDialogFactory; -import life.qbic.datamanager.views.notifications.NotificationDialog; import life.qbic.datamanager.views.projects.create.CollaboratorsLayout.ProjectCollaborators; import life.qbic.datamanager.views.projects.create.ExperimentalInformationLayout.ExperimentalInformation; import life.qbic.datamanager.views.projects.create.ProjectDesignLayout.ProjectDesign; @@ -61,7 +60,6 @@ public class AddProjectDialog extends QbicDialog { private final Button nextButton; private final Map stepContent; - private final transient CancelConfirmationDialogFactory cancelConfirmationDialogFactory; private StepIndicator addStep(Stepper stepper, String label, Component layout) { stepContent.put(label, layout); @@ -71,8 +69,7 @@ private StepIndicator addStep(Stepper stepper, String label, Component layout) { public AddProjectDialog(ProjectInformationService projectInformationService, FinanceService financeService, PersonLookupService personLookupService, - SpeciesLookupService speciesLookupService, TerminologyService terminologyService, - CancelConfirmationDialogFactory cancelConfirmationDialogFactory) { + SpeciesLookupService speciesLookupService, TerminologyService terminologyService) { super(); addClassName("add-project-dialog"); @@ -81,8 +78,6 @@ public AddProjectDialog(ProjectInformationService projectInformationService, requireNonNull(personLookupService, "personLookupService must not be null"); requireNonNull(speciesLookupService, "ontologyTermInformationService must not be null"); - this.cancelConfirmationDialogFactory = requireNonNull(cancelConfirmationDialogFactory, - "cancelConfirmationDialogFactory must not be null"); this.projectDesignLayout = new ProjectDesignLayout(projectInformationService, financeService); this.fundingInformationLayout = new FundingInformationLayout(); this.collaboratorsLayout = new CollaboratorsLayout(personLookupService); @@ -147,9 +142,14 @@ public void enableOfferSearch() { } private void onCancelClicked() { - NotificationDialog cancelConfirmationDialog = cancelConfirmationDialogFactory.cancelConfirmationDialog( - it -> close(), "project.create", getLocale()); - cancelConfirmationDialog.open(); + AlertDialog.alert(this) + .warning() + .title("Discard changes?") + .message("By aborting the editing process and closing the dialog, you will lose all information entered.") + .confirmButton("Discard changes", () -> close()) + .cancelButton("Keep editing", () -> {}) + .build() + .open(); } private void onConfirmClicked(ClickEvent