diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigFieldDisplay.razor b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigFieldDisplay.razor index 7b6e1f2..c236f22 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigFieldDisplay.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigFieldDisplay.razor @@ -34,7 +34,7 @@ @fragment(Descriptor) } - + } @@ -63,7 +63,7 @@ else + DisplayValueChanged="@HandleDefaultValueChanged" OnBlur="@HandleDefaultBlur" OnCancelRequested="@HandleCancelEdit" /> } } @@ -86,6 +86,7 @@ else private object? _valueAtActivation; private object? _draftValue; private bool _wasActive; + private bool _cancelRequested; protected override void OnParametersSet() { @@ -100,8 +101,10 @@ else _effectiveValue = pending != null ? pending.NewValue : Descriptor.GetValue(); if (!_wasActive && IsActive) _valueAtActivation = _effectiveValue; - if (_wasActive && !IsActive) + if (_wasActive && !IsActive && !_cancelRequested) _ = InvokeAsync(CommitIfChangedAsync); + if (_wasActive && !IsActive) + _cancelRequested = false; _wasActive = IsActive; } @@ -143,4 +146,12 @@ else await CommitIfChangedAsync(); } + private async Task HandleCancelEdit() + { + _cancelRequested = true; + _draftValue = null; + if (Ui != null) + await Ui.DeactivateFieldAsync(); + } + } diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigOptionItem.razor b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigOptionItem.razor index 1ef9f55..95e02f7 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigOptionItem.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigOptionItem.razor @@ -7,14 +7,14 @@ { - + } else { @DisplayValue - + } @@ -42,6 +42,8 @@ else [Parameter] public EventCallback DisplayValueChanged { get; set; } /// When Edit and not readonly: raised when the input loses focus; argument is the current value. [Parameter] public EventCallback OnBlur { get; set; } + /// When user clicks Cancel while this field is the active editor; parent should clear draft and deactivate. + [Parameter] public EventCallback OnCancelRequested { get; set; } [CascadingParameter] public EffectiveConfigDisplayMode DisplayMode { get; set; } = EffectiveConfigDisplayMode.View; private string? _editValue; diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveFlow.razor b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveFlow.razor index 59e722d..c59c9e5 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveFlow.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveFlow.razor @@ -5,7 +5,7 @@ @if (ShowModal) { - + } @code { @@ -16,6 +16,11 @@ [Parameter] public EventCallback OnSaveCompleted { get; set; } [Parameter] public IEffectiveConfigEditSession Session { get; set; } = null!; + private async Task HandleReverted() + { + await InvokeAsync(StateHasChanged); + } + private async Task HandleSave() { if (Session.PendingChanges.Count == 0) return; diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveModal.razor b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveModal.razor index 9345125..d169c37 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveModal.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveModal.razor @@ -1,6 +1,7 @@ @namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig @using DnsmasqWebUI.Models.Dnsmasq @using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig +@using DnsmasqWebUI.Infrastructure.Services.Abstractions @@ -41,8 +63,11 @@ @code { [Parameter] public IReadOnlyList PendingChanges { get; set; } = null!; [Parameter] public DnsmasqServiceStatus? Status { get; set; } + [Parameter] public IEffectiveConfigEditSession Session { get; set; } = null!; [Parameter] public EventCallback OnClose { get; set; } [Parameter] public EventCallback OnSave { get; set; } + /// Invoked after reverting a change so the parent can re-render. + [Parameter] public EventCallback OnReverted { get; set; } private string? ManagedFilePath => Status?.ManagedFilePath; @@ -53,6 +78,12 @@ return v.ToString() ?? "(not set)"; } + private async Task Revert(PendingEffectiveConfigChange change) + { + Session.RevertChange(change.SectionId, change.OptionName); + await OnReverted.InvokeAsync(); + } + private async Task Close() { await OnClose.InvokeAsync(); diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveModal.razor.css b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveModal.razor.css index 72f2530..5039b66 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveModal.razor.css +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSaveModal.razor.css @@ -7,3 +7,37 @@ .modal-backdrop { z-index: 1050; } + +.ec-save-modal-body { + padding-top: 0.25rem; +} + +.ec-save-modal-target { + font-size: 0.9rem; +} + +.ec-save-modal-changes { + margin: 0 -1rem; + padding: 0 1rem; +} + +.ec-save-modal-item:last-child { + border-bottom: 0 !important; +} + +.ec-save-modal-option { + font-weight: 500; + font-size: 0.75rem; +} + +.ec-save-modal-diff { + font-size: 0.9375rem; +} + +.ec-save-modal-override { + font-size: 0.8125rem; +} + +.ec-save-modal-empty { + font-size: 0.9rem; +} diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor index 889772e..2feb114 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor @@ -67,6 +67,7 @@ return Task.CompletedTask; }, ActivateFieldAsync = fieldKey => { ActivateField(fieldKey); return Task.CompletedTask; }, + DeactivateFieldAsync = () => { Session.DeactivateField(); return InvokeAsync(StateHasChanged); }, CommitFieldAsync = args => { HandleFieldEditCommitted(args); return Task.CompletedTask; } }; } diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigToolbar.razor b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigToolbar.razor index c23632d..4aab9e3 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigToolbar.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigToolbar.razor @@ -17,13 +17,13 @@ + @if (PendingChangesCount > 0) { @PendingChangesCount change(s) } - } else { diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/OptionStateBadge.razor b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/OptionStateBadge.razor index f86df2b..66d7db9 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/OptionStateBadge.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/OptionStateBadge.razor @@ -20,14 +20,29 @@ else if (ShowEditableBadge) { @if (EditableAsButton) { - + @if (IsActiveEditor) + { + + } + else + { + + } } else { @@ -48,10 +63,22 @@ else if (ShowEditableBadge) [Parameter] public bool HasPendingChange { get; set; } /// When true, editable badge is a button; otherwise a static span. [Parameter] public bool EditableAsButton { get; set; } = true; + /// When true, this field is the active editor; show Cancel instead of Edit and invoke OnCancelRequested on click. + [Parameter] public bool IsActiveEditor { get; set; } + /// When user clicks Cancel (while IsActiveEditor). Invoke before deactivating so parent can clear draft. + [Parameter] public EventCallback OnCancelRequested { get; set; } /// Field key for activation via context when editable badge is clicked. [Parameter] public string? FieldKey { get; set; } [CascadingParameter] public EffectiveConfigUiContext? Ui { get; set; } + private async Task OnCancelClick() + { + if (OnCancelRequested.HasDelegate) + await OnCancelRequested.InvokeAsync(); + else if (Ui != null) + await Ui.DeactivateFieldAsync(); + } + private Task OnEditableClick() { if (Ui == null || string.IsNullOrEmpty(FieldKey)) return Task.CompletedTask; diff --git a/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigUiContext.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigUiContext.cs index 91b8316..9363214 100644 --- a/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigUiContext.cs +++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigUiContext.cs @@ -10,5 +10,6 @@ public sealed class EffectiveConfigUiContext public required Func RequestReadonlyPopoverAsync { get; init; } public required Func ScheduleReadonlyPopoverCloseAsync { get; init; } public required Func ActivateFieldAsync { get; init; } + public required Func DeactivateFieldAsync { get; init; } public required Func CommitFieldAsync { get; init; } }