Eliminates the per-page <ConfirmDialog @ref="_confirmDialog" ConfirmButtonClass="btn-danger" /> boilerplate. Pages now inject IDialogService and call ConfirmAsync(title, message, danger: true) programmatically. New scoped service holds a single active dialog (throws on nested calls), with a global DialogHost mounted once in MainLayout that renders the modal markup, owns body scroll-lock via Bootstrap's modal-open class, traps focus on the modal element, and handles Escape-to-cancel. Same service also exposes PromptAsync, used to replace the bespoke NewFolderDialog. Both ConfirmDialog and NewFolderDialog components are deleted — their callers (~13 pages across Admin/Design/Deployment /Monitoring) now go through the service. DiffDialog stays as-is — different use case (before/after content). bUnit tests in TopologyPageTests, DataConnectionsPageTests, and TemplatesPageTests register IDialogService in their service collection. Also: a top-of-file Razor comment on Sites.razor pointing future implementers at it as the reference list-page pattern.
95 lines
3.6 KiB
C#
95 lines
3.6 KiB
C#
namespace ScadaLink.CentralUI.Components.Shared;
|
|
|
|
/// <summary>
|
|
/// Default <see cref="IDialogService"/> implementation. Holds the currently
|
|
/// open dialog state in <see cref="Current"/> and notifies subscribers (the
|
|
/// <c>DialogHost</c> component) via <see cref="OnChange"/>. Only a single
|
|
/// dialog can be open at a time; attempting to open another while one is
|
|
/// already active throws <see cref="InvalidOperationException"/> — there is
|
|
/// no nested-dialog use case today and surfacing the bug is preferable to
|
|
/// silently queuing.
|
|
/// </summary>
|
|
public class DialogService : IDialogService
|
|
{
|
|
/// <summary>
|
|
/// Raised whenever <see cref="Current"/> changes (dialog opened or closed).
|
|
/// The host component subscribes and calls <c>StateHasChanged</c>.
|
|
/// </summary>
|
|
public event Action? OnChange;
|
|
|
|
/// <summary>
|
|
/// The dialog currently being displayed, or <c>null</c> when no dialog is
|
|
/// open. The host reads this to decide what (if anything) to render.
|
|
/// </summary>
|
|
public DialogState? Current { get; private set; }
|
|
|
|
private TaskCompletionSource<object?>? _tcs;
|
|
|
|
public Task<bool> ConfirmAsync(string title, string message, bool danger = false)
|
|
{
|
|
EnsureNoActiveDialog();
|
|
var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
_tcs = tcs;
|
|
Current = new DialogState(title, DialogKind.Confirm, message, danger, PromptInitial: string.Empty, Placeholder: null);
|
|
OnChange?.Invoke();
|
|
return tcs.Task.ContinueWith(t => t.Result is bool b && b, TaskScheduler.Default);
|
|
}
|
|
|
|
public Task<string?> PromptAsync(string title, string label, string initialValue = "", string? placeholder = null)
|
|
{
|
|
EnsureNoActiveDialog();
|
|
var tcs = new TaskCompletionSource<object?>(TaskCreationOptions.RunContinuationsAsynchronously);
|
|
_tcs = tcs;
|
|
Current = new DialogState(title, DialogKind.Prompt, label, Danger: false, PromptInitial: initialValue, Placeholder: placeholder);
|
|
OnChange?.Invoke();
|
|
return tcs.Task.ContinueWith(t => t.Result as string, TaskScheduler.Default);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called by the host component when the user dismisses or confirms the
|
|
/// dialog. <paramref name="result"/> must be a <c>bool</c> for confirms
|
|
/// and a <c>string?</c> for prompts (null = cancel).
|
|
/// </summary>
|
|
internal void Resolve(object? result)
|
|
{
|
|
var tcs = _tcs;
|
|
_tcs = null;
|
|
Current = null;
|
|
OnChange?.Invoke();
|
|
tcs?.TrySetResult(result);
|
|
}
|
|
|
|
private void EnsureNoActiveDialog()
|
|
{
|
|
if (Current is not null)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"A dialog is already open. IDialogService does not support nested dialogs.");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Snapshot of a dialog's display state, exposed read-only on
|
|
/// <see cref="DialogService.Current"/> for the host component to render.
|
|
/// </summary>
|
|
/// <param name="Title">Modal title text.</param>
|
|
/// <param name="Kind">Discriminates between confirm and prompt rendering.</param>
|
|
/// <param name="Body">For confirm: the message; for prompt: the input label.</param>
|
|
/// <param name="Danger">When true, the confirm button uses danger styling.</param>
|
|
/// <param name="PromptInitial">Initial value for prompt-kind dialogs.</param>
|
|
/// <param name="Placeholder">Placeholder shown when the prompt input is empty.</param>
|
|
public record DialogState(
|
|
string Title,
|
|
DialogKind Kind,
|
|
string Body,
|
|
bool Danger,
|
|
string PromptInitial,
|
|
string? Placeholder);
|
|
|
|
public enum DialogKind
|
|
{
|
|
Confirm,
|
|
Prompt
|
|
}
|