Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions datamanager-app/front-end-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
<<builder>>
+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 {
<<interface>>
+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.



Binary file modified datamanager-app/src/main/bundles/dev.bundle
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Comment thread
KochTobi marked this conversation as resolved.
}

private void onAddTokenClicked(AddTokenEvent addTokenEvent) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,16 @@
* <p>Two entry points:</p>
* <pre>{@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();
* }</pre>
*
Expand Down Expand Up @@ -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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These if else conditions check if it is confirm only and if not, if the confirm button should be marked as dangerous. As there cannot be a dangerous confirm only dialog (intent DANGER or WARNING and no cancel action), should this be checked explicitly? I think it is very sensible to forbid a dangerous operation where you can only confirm.

DialogFooter.withDangerousConfirm(dialog, cancelLabel, confirmLabel);
} else if (cancelLabel != null && cancelAction != null) {
DialogFooter.with(dialog, cancelLabel, confirmLabel);
Expand Down Expand Up @@ -282,38 +283,56 @@ 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.
*
* <p>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.</p>
* <p>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.</p>
*
* <p>Usage:</p>
* <pre>{@code
* AlertDialog.danger(this,
* "Remove dataset connection?",
* "This will disconnect the dataset from the project.",
* "Disconnect dataset",
* "Keep connection",
* () -> performRemove())
* .open();
* }</pre>
*
* @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
*/
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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -92,16 +90,13 @@ public ProjectSideNavigationComponent(
UserPermissions userPermissions,
SpeciesLookupService speciesLookupService,
TerminologyService terminologyService,
CancelConfirmationDialogFactory cancelConfirmationDialogFactory,
MessageSourceNotificationFactory messageSourceNotificationFactory) {
content = new Div();
requireNonNull(projectInformationService);
requireNonNull(experimentInformationService);
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");
Expand Down Expand Up @@ -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) {
Expand Down
Loading
Loading