fix(ui): replace inline save indicator with Bootstrap toast notification

SettingBox base now uses JS interop to call showToast() which
renders a Bootstrap Toast at the bottom-right corner. Removed
all SaveIndicator RenderFragments and IsSaving/IsSaved state
from the four child components. Toast auto-hides after 2s.
This commit is contained in:
REDCODE
2026-07-09 02:58:01 +02:00
parent ea25eca9fa
commit 584e24d751
6 changed files with 26 additions and 60 deletions

View File

@@ -1,7 +1,9 @@
@using Butter.Dtos.Settings
@using MilkStream.Client.Services
@using Microsoft.JSInterop
@inject SettingsService SettingsService
@inject IJSRuntime JSRuntime
@code {
[Parameter]
@@ -9,9 +11,6 @@
private CancellationTokenSource? _debounceCts;
protected bool IsSaving { get; set; }
protected bool IsSaved { get; set; }
protected virtual SettingDto? GetSetting() {
return Setting;
}
@@ -28,18 +27,11 @@
var setting = GetSetting();
if (setting == null) return;
IsSaving = true;
await InvokeAsync(StateHasChanged);
await JSRuntime.InvokeVoidAsync("showToast", "Saving\u2026", "text-bg-secondary");
await SettingsService.UpdateSettingAsync(setting);
IsSaving = false;
IsSaved = true;
await InvokeAsync(StateHasChanged);
await Task.Delay(2000);
IsSaved = false;
await InvokeAsync(StateHasChanged);
await JSRuntime.InvokeVoidAsync("showToast", "Saved", "text-bg-success");
} catch (OperationCanceledException) {
// Debounce cancelled by another change
}

View File

@@ -10,7 +10,6 @@
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
<div class="d-flex align-items-center gap-2">
<input type="number" id="@Setting?.Name" class="form-control text-end flex-grow-1" @bind="currentValue" @bind:after="() => OnValueChanged()" />
@SaveIndicator
</div>
</div>
</div>
@@ -30,15 +29,4 @@
return Setting;
}
private RenderFragment SaveIndicator => builder =>
{
string cls = "";
string text = "";
if (IsSaving) { cls = "text-secondary"; text = "Saving..."; }
else if (IsSaved) { cls = "text-success"; text = "Saved"; }
builder.OpenElement(0, "small");
builder.AddAttribute(1, "class", cls);
builder.AddContent(2, text);
builder.CloseElement();
};
}

View File

@@ -12,7 +12,6 @@
<input type="range" id="@Setting?.Name" class="form-range flex-grow-1"
@bind:event="oninput" @bind="@currentValue" @bind:after="() => OnValueChanged()"
min="@effectiveMin" max="@effectiveMax" step="@Step"/>
@SaveIndicator
</div>
</div>
</div>
@@ -56,15 +55,4 @@
return Setting;
}
private RenderFragment SaveIndicator => builder =>
{
string cls = "";
string text = "";
if (IsSaving) { cls = "text-secondary"; text = "Saving..."; }
else if (IsSaved) { cls = "text-success"; text = "Saved"; }
builder.OpenElement(0, "small");
builder.AddAttribute(1, "class", cls);
builder.AddContent(2, text);
builder.CloseElement();
};
}

View File

@@ -9,7 +9,6 @@
<p class="card-text">@(Setting?.Description ?? "No description provided")</p>
<div class="d-flex align-items-center gap-2">
<input type="text" id="@Setting?.Name" class="form-control" @bind="@currentValue" @bind:after="() => OnValueChanged()" />
@SaveIndicator
</div>
</div>
</div>
@@ -28,15 +27,4 @@
return Setting;
}
private RenderFragment SaveIndicator => builder =>
{
string cls = "";
string text = "";
if (IsSaving) { cls = "text-secondary"; text = "Saving..."; }
else if (IsSaved) { cls = "text-success"; text = "Saved"; }
builder.OpenElement(0, "small");
builder.AddAttribute(1, "class", cls);
builder.AddContent(2, text);
builder.CloseElement();
};
}

View File

@@ -12,7 +12,6 @@
<div class="form-switch">
<input type="checkbox" id="@Setting?.Name" class="form-check-input m-1" @bind="isChecked" @bind:after="() => OnValueChanged()"/>
</div>
@SaveIndicator
</div>
</div>
</div>
@@ -32,15 +31,4 @@
return Setting;
}
private RenderFragment SaveIndicator => builder =>
{
string cls = "";
string text = "";
if (IsSaving) { cls = "text-secondary"; text = "Saving..."; }
else if (IsSaved) { cls = "text-success"; text = "Saved"; }
builder.OpenElement(0, "small");
builder.AddAttribute(1, "class", cls);
builder.AddContent(2, text);
builder.CloseElement();
};
}

View File

@@ -226,3 +226,25 @@ window.justifiedLayout = {
container.style.height = Math.max(top - gap, 0) + 'px';
}
};
window.showToast = function (message, cls) {
let container = document.querySelector('.settings-toast-container');
if (!container) {
container = document.createElement('div');
container.className = 'settings-toast-container toast-container position-fixed bottom-0 end-0 p-3';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.className = 'toast align-items-center border-0 ' + (cls || '');
toast.setAttribute('role', 'alert');
toast.setAttribute('aria-live', 'assertive');
toast.setAttribute('aria-atomic', 'true');
toast.innerHTML = '<div class="d-flex"><div class="toast-body">' + message + '</div><button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast"></button></div>';
container.appendChild(toast);
const bsToast = new bootstrap.Toast(toast, { delay: 2000 });
bsToast.show();
toast.addEventListener('hidden.bs.toast', function () {
toast.remove();
if (container.children.length === 0) container.remove();
});
};