refactor(ui/shared): introduce IDialogService + DialogHost

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.
This commit is contained in:
Joseph Doherty
2026-05-12 03:57:37 -04:00
parent e21791adb0
commit 8038aa7cb5
21 changed files with 351 additions and 260 deletions

View File

@@ -19,11 +19,11 @@
@inject AuthenticationStateProvider AuthStateProvider
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
@inject IDialogService Dialog
@implements IDisposable
<div class="container-fluid mt-3">
<ToastNotification @ref="_toast" />
<ConfirmDialog @ref="_confirmDialog" ConfirmButtonClass="btn-danger" />
<CreateAreaDialog @bind-IsVisible="_showCreateAreaDialog"
RequireSitePicker="_createAreaRequireSitePicker"
@@ -127,7 +127,6 @@
private string _searchText = string.Empty;
private ToastNotification _toast = default!;
private ConfirmDialog _confirmDialog = default!;
private DiffDialog _diffDialog = default!;
// ---- Live updates ----
@@ -669,9 +668,10 @@
// ---- Area & instance deletion ----
private async Task DeleteArea(TopoNode node)
{
var confirmed = await _confirmDialog.ShowAsync(
var confirmed = await Dialog.ConfirmAsync(
"Delete Area",
$"Delete area '{node.Label}'? This will fail if it has sub-areas or assigned instances.",
"Delete Area");
danger: true);
if (!confirmed) return;
_actionInProgress = true;
@@ -691,9 +691,10 @@
private async Task DeleteInstance(Instance inst)
{
var confirmed = await _confirmDialog.ShowAsync(
var confirmed = await Dialog.ConfirmAsync(
"Delete Instance",
$"Delete instance '{inst.UniqueName}'? This will remove it from the site. Store-and-forward messages will NOT be cleared.",
"Delete Instance");
danger: true);
if (!confirmed) return;
_actionInProgress = true;
@@ -745,9 +746,10 @@
private async Task DisableInstance(Instance inst)
{
var confirmed = await _confirmDialog.ShowAsync(
var confirmed = await Dialog.ConfirmAsync(
"Disable Instance",
$"Disable instance '{inst.UniqueName}'? The instance actor will be stopped.",
"Disable Instance");
danger: true);
if (!confirmed) return;
_actionInProgress = true;