fix(central-ui): resolve CentralUI-007..014 — nav authz, UTC date filters, disposal guards, N+1 fix, async script analysis
This commit is contained in:
@@ -150,6 +150,11 @@
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// CentralUI-011: if the dialog is disposed while still open (the user
|
||||
// navigated away), complete the pending task so the awaiting caller
|
||||
// resumes deterministically instead of hanging forever.
|
||||
_tcs?.TrySetResult(false);
|
||||
|
||||
if (_bodyLocked)
|
||||
{
|
||||
await TryUnlockBodyAsync();
|
||||
|
||||
@@ -30,6 +30,16 @@
|
||||
private readonly List<ToastItem> _toasts = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
// Cancels all pending auto-dismiss delays when the component is disposed
|
||||
// (CentralUI-010) so their continuations never touch a disposed component.
|
||||
private readonly CancellationTokenSource _disposalCts = new();
|
||||
|
||||
/// <summary>Number of toasts currently displayed.</summary>
|
||||
public int ToastCount
|
||||
{
|
||||
get { lock (_lock) { return _toasts.Count; } }
|
||||
}
|
||||
|
||||
public void ShowSuccess(string message, string title = "Success", int? autoDismissMs = null)
|
||||
{
|
||||
AddToast(title, message, ToastType.Success, autoDismissMs);
|
||||
@@ -52,6 +62,9 @@
|
||||
|
||||
private void AddToast(string title, string message, ToastType type, int? autoDismissMs)
|
||||
{
|
||||
// If the component is already disposed, do not add or schedule anything.
|
||||
if (_disposalCts.IsCancellationRequested) return;
|
||||
|
||||
var toast = new ToastItem { Title = title, Message = message, Type = type };
|
||||
lock (_lock)
|
||||
{
|
||||
@@ -60,14 +73,41 @@
|
||||
StateHasChanged();
|
||||
|
||||
var dismissMs = autoDismissMs ?? DefaultAutoDismissMs;
|
||||
_ = Task.Delay(dismissMs).ContinueWith(_ =>
|
||||
_ = AutoDismissAsync(toast, dismissMs, _disposalCts.Token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a toast after its dismiss delay. The delay is bound to the
|
||||
/// component's disposal token (CentralUI-010): if the host page is disposed
|
||||
/// first, the delay is cancelled and the continuation never touches the
|
||||
/// disposed component — no <see cref="ObjectDisposedException"/> escapes.
|
||||
/// </summary>
|
||||
private async Task AutoDismissAsync(ToastItem toast, int dismissMs, CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_toasts.Remove(toast);
|
||||
}
|
||||
InvokeAsync(StateHasChanged);
|
||||
});
|
||||
await Task.Delay(dismissMs, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (token.IsCancellationRequested) return;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_toasts.Remove(toast);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Component disposed between the token check and the render — ignore.
|
||||
}
|
||||
}
|
||||
|
||||
private void Dismiss(ToastItem toast)
|
||||
@@ -87,7 +127,11 @@
|
||||
_ => "bg-secondary text-white"
|
||||
};
|
||||
|
||||
public void Dispose() { }
|
||||
public void Dispose()
|
||||
{
|
||||
_disposalCts.Cancel();
|
||||
_disposalCts.Dispose();
|
||||
}
|
||||
|
||||
private enum ToastType { Success, Error, Warning, Info }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user