Add reusable UI components and toast notification system
- Introduce ActionPanel, EnvelopeInfoCard, TfaForm, ConfirmDialog, StatusPage, and Toast components for modular, presentational UI - Add ToastService for pub/sub toast notifications; register in DI - Refactor AccessCodeForm for improved UX and parameterization - Enhance MainLayout with Toast integration and better error handling - Standardize and extend app.css for new components and responsive design - All new components are "dumb" (no service/API knowledge), using EventCallbacks for parent interaction - ConfirmDialog supports awaitable user confirmation via TaskCompletionSource
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
@* ConfirmDialog: Ersetzt SweetAlert2 aus dem Web-Projekt.
|
||||
|
||||
Zeigt einen modalen Bestätigungsdialog mit Titel, Text und Ja/Nein-Buttons.
|
||||
Wird NICHT über Parameter gesteuert, sondern über eine Methode:
|
||||
|
||||
var confirmed = await _dialog.ShowAsync("Titel", "Text");
|
||||
|
||||
WARUM eine Methode statt Parameter?
|
||||
- Ein Dialog ist ein "einmaliges Ereignis", kein dauerhafter Zustand
|
||||
- Die aufrufende Komponente will auf das Ergebnis WARTEN (await)
|
||||
- Mit Parametern müsste man den State manuell hin- und herschalten
|
||||
|
||||
WARUM kein SweetAlert2?
|
||||
- SweetAlert2 ist eine JavaScript-Bibliothek
|
||||
- In Blazor können wir das nativ in C# lösen, ohne JS-Interop
|
||||
- Weniger Abhängigkeiten = weniger Wartung = weniger Fehlerquellen *@
|
||||
|
||||
@if (_isVisible)
|
||||
{
|
||||
@* Hintergrund-Overlay: Dunkler Hintergrund hinter dem Dialog.
|
||||
Im Web-Projekt macht SweetAlert2 das automatisch.
|
||||
Hier bauen wir es selbst mit CSS. *@
|
||||
<div class="modal-backdrop fade show"></div>
|
||||
|
||||
@* Modal-Dialog: Bootstrap 5 Modal-Markup.
|
||||
Normalerweise braucht Bootstrap-Modal JavaScript (bootstrap.js)
|
||||
um zu öffnen/schließen. Wir steuern die Sichtbarkeit stattdessen
|
||||
über die _isVisible-Variable — Blazor macht das DOM-Update. *@
|
||||
<div class="modal fade show d-block" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
|
||||
@* Header: Titel + Schließen-Button *@
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@_title</h5>
|
||||
<button type="button" class="btn-close" @onclick="Cancel"></button>
|
||||
</div>
|
||||
|
||||
@* Body: Beschreibungstext *@
|
||||
<div class="modal-body">
|
||||
<p>@_message</p>
|
||||
</div>
|
||||
|
||||
@* Footer: Abbrechen + Bestätigen *@
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" @onclick="Cancel">
|
||||
@_cancelText
|
||||
</button>
|
||||
<button type="button" class="btn btn-@_confirmColor" @onclick="Confirm">
|
||||
@_confirmText
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
// ── Interner State (NICHT Parameter — wird über ShowAsync gesetzt) ──
|
||||
|
||||
private bool _isVisible;
|
||||
private string _title = string.Empty;
|
||||
private string _message = string.Empty;
|
||||
private string _confirmText = "Ja";
|
||||
private string _cancelText = "Abbrechen";
|
||||
private string _confirmColor = "primary";
|
||||
|
||||
/// <summary>
|
||||
/// TaskCompletionSource: Das Herzstück dieser Komponente.
|
||||
///
|
||||
/// WAS IST DAS?
|
||||
/// Ein TaskCompletionSource erstellt einen Task, der erst dann
|
||||
/// "fertig" wird, wenn jemand SetResult() aufruft.
|
||||
///
|
||||
/// WIE FUNKTIONIERT ES?
|
||||
/// 1. ShowAsync() erstellt ein neues TaskCompletionSource
|
||||
/// 2. ShowAsync() gibt dessen Task zurück → der Aufrufer wartet (await)
|
||||
/// 3. Der Benutzer klickt "Ja" → Confirm() ruft SetResult(true) auf
|
||||
/// 4. Der await in der aufrufenden Komponente kommt zurück mit true
|
||||
///
|
||||
/// Das ist wie ein "Promise" in JavaScript, nur in C#.
|
||||
/// </summary>
|
||||
private TaskCompletionSource<bool>? _tcs;
|
||||
|
||||
/// <summary>
|
||||
/// Zeigt den Dialog und wartet auf die Benutzer-Entscheidung.
|
||||
///
|
||||
/// Beispiel-Aufruf:
|
||||
/// var confirmed = await _dialog.ShowAsync(
|
||||
/// "Unterschreiben",
|
||||
/// "Möchten Sie das Dokument wirklich unterschreiben?");
|
||||
/// </summary>
|
||||
/// <param name="title">Dialog-Überschrift</param>
|
||||
/// <param name="message">Beschreibungstext</param>
|
||||
/// <param name="confirmText">Text auf dem Bestätigen-Button (Standard: "Ja")</param>
|
||||
/// <param name="cancelText">Text auf dem Abbrechen-Button (Standard: "Abbrechen")</param>
|
||||
/// <param name="confirmColor">Bootstrap-Farbe des Bestätigen-Buttons (Standard: "primary")</param>
|
||||
/// <returns>true wenn bestätigt, false wenn abgebrochen</returns>
|
||||
public Task<bool> ShowAsync(
|
||||
string title,
|
||||
string message,
|
||||
string confirmText = "Ja",
|
||||
string cancelText = "Abbrechen",
|
||||
string confirmColor = "primary")
|
||||
{
|
||||
_title = title;
|
||||
_message = message;
|
||||
_confirmText = confirmText;
|
||||
_cancelText = cancelText;
|
||||
_confirmColor = confirmColor;
|
||||
|
||||
_tcs = new TaskCompletionSource<bool>();
|
||||
_isVisible = true;
|
||||
StateHasChanged();
|
||||
|
||||
return _tcs.Task;
|
||||
}
|
||||
|
||||
private void Confirm()
|
||||
{
|
||||
_isVisible = false;
|
||||
_tcs?.TrySetResult(true);
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void Cancel()
|
||||
{
|
||||
_isVisible = false;
|
||||
_tcs?.TrySetResult(false);
|
||||
StateHasChanged();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
@* StatusPage: Wiederverwendbare Status-Seite für alle Endzustände.
|
||||
Ersetzt EnvelopeSigned.cshtml, EnvelopeRejected.cshtml, Error-Views und Expired.
|
||||
"Dumme" Komponente: Keine Services, keine API-Calls, nur Parameter → Anzeige. *@
|
||||
|
||||
<div class="page">
|
||||
<header class="text-center">
|
||||
@switch (Type)
|
||||
{
|
||||
case "signed":
|
||||
<div class="status-icon signed">
|
||||
<i class="bi bi-check-circle"></i>
|
||||
</div>
|
||||
<h1>Dokument erfolgreich unterschrieben</h1>
|
||||
<p class="text-muted">
|
||||
Sie erhalten eine Bestätigung per E-Mail, sobald alle Empfänger unterschrieben haben.
|
||||
</p>
|
||||
break;
|
||||
|
||||
case "rejected":
|
||||
<div class="status-icon rejected">
|
||||
<i class="bi bi-x-circle"></i>
|
||||
</div>
|
||||
<h1>Dokument wurde abgelehnt</h1>
|
||||
<p class="text-muted">
|
||||
@if (!string.IsNullOrEmpty(Title) && !string.IsNullOrEmpty(SenderEmail))
|
||||
{
|
||||
<span>
|
||||
Das Dokument «@Title» wurde abgelehnt.
|
||||
Bei Fragen wenden Sie sich an
|
||||
<a href="mailto:@SenderEmail">@SenderEmail</a>.
|
||||
</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Dieses Dokument wurde von einem Empfänger abgelehnt.</span>
|
||||
}
|
||||
</p>
|
||||
break;
|
||||
|
||||
case "not_found":
|
||||
<div class="status-icon locked">
|
||||
<i class="bi bi-question-circle"></i>
|
||||
</div>
|
||||
<h1>Dokument nicht gefunden</h1>
|
||||
<p class="text-muted">
|
||||
Dieses Dokument existiert nicht oder ist nicht mehr verfügbar.
|
||||
Wenn Sie diese URL per E-Mail erhalten haben, wenden Sie sich bitte an das IT-Team.
|
||||
</p>
|
||||
break;
|
||||
|
||||
case "expired":
|
||||
<div class="status-icon locked">
|
||||
<i class="bi bi-clock-history"></i>
|
||||
</div>
|
||||
<h1>Link abgelaufen</h1>
|
||||
<p class="text-muted">
|
||||
Der Zugang zu diesem Dokument ist abgelaufen.
|
||||
</p>
|
||||
break;
|
||||
}
|
||||
</header>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
/// <summary>
|
||||
/// Bestimmt welche Status-Variante angezeigt wird.
|
||||
/// Erlaubte Werte: "signed", "rejected", "not_found", "expired"
|
||||
/// </summary>
|
||||
[Parameter, EditorRequired]
|
||||
public string Type { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>E-Mail des Absenders — nur bei "rejected" relevant.</summary>
|
||||
[Parameter]
|
||||
public string? SenderEmail { get; set; }
|
||||
|
||||
/// <summary>Titel des Umschlags — nur bei "rejected" relevant.</summary>
|
||||
[Parameter]
|
||||
public string? Title { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
@* Toast: Zeigt kurze Benachrichtigungen an. Ersetzt AlertifyJS aus dem Web-Projekt.
|
||||
|
||||
Wird einmal im MainLayout platziert. Hört auf den ToastService.
|
||||
Wenn eine Komponente irgendwo _toast.ShowSuccess("Text") aufruft,
|
||||
erscheint hier ein Toast und verschwindet nach 4 Sekunden automatisch.
|
||||
|
||||
Bootstrap Toast-Klassen werden genutzt:
|
||||
- toast-container: Positionierung (oben rechts)
|
||||
- toast: Einzelner Toast
|
||||
- bg-success/bg-danger/etc: Farbe basierend auf Typ *@
|
||||
|
||||
@inject ToastService ToastService
|
||||
@implements IDisposable
|
||||
|
||||
@if (ToastService.Messages.Count > 0)
|
||||
{
|
||||
@* toast-container: Bootstrap-Klasse die Toasts oben rechts positioniert.
|
||||
position-fixed: Bleibt an der Stelle auch beim Scrollen.
|
||||
z-index 1080: Über dem Modal-Backdrop (1070) damit Toasts
|
||||
auch während eines ConfirmDialogs sichtbar sind. *@
|
||||
<div class="toast-container position-fixed top-0 end-0 p-3" style="z-index: 1080;">
|
||||
@foreach (var message in ToastService.Messages)
|
||||
{
|
||||
<div class="toast show align-items-center text-white bg-@message.Type border-0"
|
||||
role="alert">
|
||||
<div class="d-flex">
|
||||
<div class="toast-body">
|
||||
<i class="bi @message.IconClass me-2"></i>
|
||||
@message.Text
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn-close btn-close-white me-2 m-auto"
|
||||
@onclick="() => ToastService.Dismiss(message)">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
// Subscriber: Wenn der ToastService eine Änderung meldet,
|
||||
// zeichnet diese Komponente sich neu (StateHasChanged).
|
||||
// So erscheinen/verschwinden Toasts automatisch.
|
||||
ToastService.OnChange += HandleChange;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InvokeAsync ist nötig weil OnChange von einem
|
||||
/// async void (dem Timer im ToastService) ausgelöst wird.
|
||||
/// Das kann auf einem anderen Thread passieren.
|
||||
/// InvokeAsync wechselt zurück auf den Blazor UI-Thread.
|
||||
/// </summary>
|
||||
private async void HandleChange()
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event-Handler abmelden wenn die Komponente entfernt wird.
|
||||
/// Ohne das würde der ToastService eine Referenz auf eine
|
||||
/// nicht mehr existierende Komponente halten → Memory Leak.
|
||||
/// </summary>
|
||||
public void Dispose()
|
||||
{
|
||||
ToastService.OnChange -= HandleChange;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user