EffectiveConfig: fix pending null display, OptionStateBadge, OptionHelpLabel

- Use pending != null ? pending.NewValue : Descriptor.GetValue() so clearing
  a value (port, log-queries, local-service) shows '(not set)' instead of
  reverting to the old value; fix effective value and commit logic in
  EffectiveConfigFieldDisplay.
- Rename ReadonlyBadge to OptionStateBadge; add editable/pending state and
  use it from EffectiveConfigOptionItem, EffectiveConfigFieldDisplay,
  EffectiveConfigMultiValueItem.
- OptionHelpLabel: default CssClass from HelpKey (EffectiveCssClass).
This commit is contained in:
2026-02-28 19:04:38 +10:00
parent 6a7c0f62e1
commit 4e122ba439
7 changed files with 80 additions and 67 deletions
@@ -14,9 +14,9 @@
var showEditableBadge = !isReadonly;
var isActiveEditor = IsEditMode && IsActive;
var fragmentWithCallback = isActiveEditor ? _fragmentWithCallback : null;
var effectiveValue = _effectiveValue ?? Descriptor.GetValue();
var effectiveValue = _effectiveValue;
<li class="ec-field @(isActiveEditor ? "ec-field-editing" : "")" title="@(Descriptor.GetSource()?.GetReadOnlyTooltip())">
<OptionHelpLabel AnchorId="@_anchorId" HelpKey="@_optionHelpKey" OptionLabel="@Descriptor.OptionName" CssClass="@(_optionHelpKey != null ? "ec-field-label ec-field-label-help" : "ec-field-label")" Title="@(labelTooltip ?? Descriptor.GetSource()?.GetReadOnlyTooltip())"><strong>@Descriptor.OptionName:</strong></OptionHelpLabel>
<OptionHelpLabel AnchorId="@_anchorId" HelpKey="@_optionHelpKey" OptionLabel="@Descriptor.OptionName" Title="@(labelTooltip ?? Descriptor.GetSource()?.GetReadOnlyTooltip())"><strong>@Descriptor.OptionName:</strong></OptionHelpLabel>
<span class="ec-field-value">
@if (isActiveEditor)
{
@@ -34,11 +34,7 @@
@fragment(Descriptor)
</CascadingValue>
}
<ReadonlyBadge AnchorId="@_readonlyBadgeAnchorId" Source="@Descriptor.GetSource()" OptionName="@Descriptor.OptionName" Value="@effectiveValue" IsInteractive="@IsEditMode" />
@if (showEditableBadge)
{
<button type="button" class="badge ec-badge-inline border-0 ms-1 @(_hasPendingChange ? "bg-warning text-dark" : "bg-primary")" @onclick="OnActivateEdit" @onclick:stopPropagation="true" title="@(_hasPendingChange ? "Pending change" : "Edit this field")" aria-label="Edit">@(_hasPendingChange ? "pending" : "editable")</button>
}
<OptionStateBadge AnchorId="@_readonlyBadgeAnchorId" Source="@Descriptor.GetSource()" OptionName="@Descriptor.OptionName" Value="@effectiveValue" IsInteractive="@IsEditMode" ShowEditableBadge="@showEditableBadge" HasPendingChange="@_hasPendingChange" EditableAsButton="true" OnActivateEdit="@OnActivateEdit" />
</span>
</li>
}
@@ -49,7 +45,7 @@ else if (Descriptor.IsMultiValue)
}
else
{
var value = _effectiveValue ?? Descriptor.GetValue();
var value = _effectiveValue;
var displayValue = value == null ? "(not set)" : (value is IReadOnlyList<string> list ? (list.Count == 0 ? "(none)" : string.Join(", ", list)) : value.ToString());
var isReadonly = Descriptor.GetSource()?.IsReadOnly == true;
var showReadonlyHint = IsEditMode && isReadonly;
@@ -103,7 +99,7 @@ else
string.Equals(c.SectionId, Descriptor.SectionId, StringComparison.OrdinalIgnoreCase) &&
string.Equals(c.OptionName, Descriptor.OptionName, StringComparison.OrdinalIgnoreCase));
_hasPendingChange = pending != null;
_effectiveValue = pending?.NewValue ?? Descriptor.GetValue();
_effectiveValue = pending != null ? pending.NewValue : Descriptor.GetValue();
if (!_wasActive && IsActive)
_valueAtActivation = _effectiveValue;
if (_wasActive && !IsActive)
@@ -124,7 +120,7 @@ else
private async Task CommitIfChangedAsync()
{
var newValue = _draftValue ?? _effectiveValue ?? Descriptor.GetValue();
var newValue = _draftValue ?? _effectiveValue;
if (!Equals(_valueAtActivation, newValue))
{
var source = Descriptor.GetSource();
@@ -4,11 +4,7 @@
<span class="ec-field-value-item" title="@(Source?.GetReadOnlyTooltip())">
@Value
<ReadonlyBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@OptionName" Value="@Value" IsInteractive="@(IsEditMode && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" />
@if (Source?.IsReadOnly != true && IsEditMode)
{
<span class="badge bg-primary ec-badge-inline ms-1">editable</span>
}
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@OptionName" Value="@Value" IsInteractive="@(IsEditMode && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" ShowEditableBadge="@(Source?.IsReadOnly != true && IsEditMode)" EditableAsButton="false" />
</span>
@code {
@@ -3,7 +3,7 @@
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
<li class="ec-field">
<OptionHelpLabel AnchorId="@(OptionHelpAnchorId ?? "")" HelpKey="@OptionHelpKey" OptionLabel="@Label" CssClass="@(OptionHelpKey != null ? "ec-field-label ec-field-label-help" : "ec-field-label")" Title="@LabelTooltip"><strong>@Label:</strong></OptionHelpLabel>
<OptionHelpLabel AnchorId="@(OptionHelpAnchorId ?? "")" HelpKey="@OptionHelpKey" OptionLabel="@Label" Title="@LabelTooltip"><strong>@Label:</strong></OptionHelpLabel>
@if (IsActiveEditor)
{
<span class="ec-field-value ec-field-value-multi" @onclick:stopPropagation="true">
@@ -2,26 +2,19 @@
@using DnsmasqWebUI.Models.Dnsmasq
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
<OptionHelpLabel AnchorId="@(OptionHelpAnchorId ?? "")" HelpKey="@OptionHelpKey" OptionLabel="@Label" CssClass="@(OptionHelpKey != null ? "ec-field-label ec-field-label-help" : "ec-field-label")" Title="@(LabelTooltip ?? Source?.GetReadOnlyTooltip())"><strong>@Label:</strong></OptionHelpLabel>
<OptionHelpLabel AnchorId="@(OptionHelpAnchorId ?? "")" HelpKey="@OptionHelpKey" OptionLabel="@Label" Title="@(LabelTooltip ?? Source?.GetReadOnlyTooltip())"><strong>@Label:</strong></OptionHelpLabel>
@if (DisplayMode == EffectiveConfigDisplayMode.Edit && IsActive && Source?.IsReadOnly != true)
{
<span class="ec-field-value" @onclick:stopPropagation="true">
<input type="text" class="form-control form-control-sm" value="@(_editValue ?? DisplayValue)" @oninput="OnValueInput" @onfocusout="OnInputBlur" />
@if (ShowEditableBadge)
{
<button type="button" class="badge ec-badge-inline border-0 ms-1 @(HasPendingChange ? "bg-warning text-dark" : "bg-primary")" @onclick="OnActivateEdit" @onclick:stopPropagation="true" title="@(HasPendingChange ? "Pending change" : "Edit this field")" aria-label="Edit">@(HasPendingChange ? "pending" : "editable")</button>
}
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@Label" Value="@(RawValue ?? DisplayValue)" IsInteractive="@(DisplayMode == EffectiveConfigDisplayMode.Edit && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" ShowEditableBadge="@ShowEditableBadge" HasPendingChange="@HasPendingChange" EditableAsButton="true" OnActivateEdit="@OnActivateEdit" />
</span>
}
else
{
<span class="ec-field-value">
@DisplayValue
<ReadonlyBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@Label" Value="@(RawValue ?? DisplayValue)" IsInteractive="@(DisplayMode == EffectiveConfigDisplayMode.Edit && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" />
@if (ShowEditableBadge && Source?.IsReadOnly != true)
{
<button type="button" class="badge ec-badge-inline border-0 ms-1 @(HasPendingChange ? "bg-warning text-dark" : "bg-primary")" @onclick="OnActivateEdit" @onclick:stopPropagation="true" title="@(HasPendingChange ? "Pending change" : "Edit this field")" aria-label="Edit">@(HasPendingChange ? "pending" : "editable")</button>
}
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@Label" Value="@(RawValue ?? DisplayValue)" IsInteractive="@(DisplayMode == EffectiveConfigDisplayMode.Edit && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" ShowEditableBadge="@ShowEditableBadge" HasPendingChange="@HasPendingChange" EditableAsButton="true" OnActivateEdit="@OnActivateEdit" />
</span>
}
@@ -1,7 +1,7 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
<span id="@AnchorId" class="@CssClass" title="@Title" @onclick="OnClick" @onmouseleave="OnMouseLeave">
<span id="@AnchorId" class="@EffectiveCssClass" title="@Title" @onclick="OnClick" @onmouseleave="OnMouseLeave">
@ChildContent
</span>
@@ -10,6 +10,9 @@
[Parameter] public string? HelpKey { get; set; }
[Parameter] public string OptionLabel { get; set; } = "";
[Parameter] public string? CssClass { get; set; }
private string EffectiveCssClass =>
CssClass ?? (!string.IsNullOrEmpty(HelpKey) ? "ec-field-label ec-field-label-help" : "ec-field-label");
[Parameter] public string? Title { get; set; }
[CascadingParameter] public EffectiveConfigUiContext? Ui { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
@@ -0,0 +1,65 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
@if (Source?.IsReadOnly == true)
{
@if (IsInteractive && Ui != null)
{
<button type="button" id="@AnchorId" class="badge bg-secondary ec-badge-inline border-0 ms-1" title="@(Source?.GetReadOnlyTooltip())"
@onclick="OnReadonlyClick" @onmouseleave="OnReadonlyMouseLeave">
readonly
</button>
}
else
{
<span class="badge bg-secondary ec-badge-inline ms-1" title="@(Source?.GetReadOnlyTooltip())">readonly</span>
}
}
else if (ShowEditableBadge)
{
@if (EditableAsButton)
{
<button type="button"
class="badge ec-badge-inline border-0 ms-1 @(HasPendingChange ? "bg-warning text-dark" : "bg-primary")"
@onclick="OnActivateEdit"
@onclick:stopPropagation="true"
title="@(HasPendingChange ? "Pending change" : "Edit this field")"
aria-label="Edit">
@(HasPendingChange ? "pending" : "editable")
</button>
}
else
{
<span class="badge bg-primary ec-badge-inline ms-1">editable</span>
}
}
@code {
[Parameter] public string AnchorId { get; set; } = "";
[Parameter] public ConfigValueSource? Source { get; set; }
[Parameter] public string OptionName { get; set; } = "";
[Parameter] public object? Value { get; set; }
/// <summary>When true, render readonly as button and raise click/mouseleave via context.</summary>
[Parameter] public bool IsInteractive { get; set; }
/// <summary>When true, show editable/pending badge for non-readonly fields.</summary>
[Parameter] public bool ShowEditableBadge { get; set; }
/// <summary>When true, badge shows "pending" and warning style.</summary>
[Parameter] public bool HasPendingChange { get; set; }
/// <summary>When true, editable badge is a button; otherwise a static span.</summary>
[Parameter] public bool EditableAsButton { get; set; } = true;
[Parameter] public EventCallback OnActivateEdit { get; set; }
[CascadingParameter] public EffectiveConfigUiContext? Ui { get; set; }
private Task OnReadonlyClick()
{
if (Ui == null || Source == null) return Task.CompletedTask;
return Ui.RequestReadonlyPopoverAsync(new ReadonlyBadgeClickedEventArgs(AnchorId, Source, OptionName, Value));
}
private Task OnReadonlyMouseLeave()
{
if (Ui == null) return Task.CompletedTask;
return Ui.ScheduleReadonlyPopoverCloseAsync();
}
}
@@ -1,40 +0,0 @@
@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Models.Dnsmasq
@using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig
@if (Source?.IsReadOnly == true)
{
@if (IsInteractive && Ui != null)
{
<button type="button" id="@AnchorId" class="badge bg-secondary ec-badge-inline border-0 ms-1" title="@(Source?.GetReadOnlyTooltip())"
@onclick="OnClick" @onmouseleave="OnMouseLeave">
readonly
</button>
}
else
{
<span class="badge bg-secondary ec-badge-inline ms-1" title="@(Source?.GetReadOnlyTooltip())">readonly</span>
}
}
@code {
[Parameter] public string AnchorId { get; set; } = "";
[Parameter] public ConfigValueSource? Source { get; set; }
[Parameter] public string OptionName { get; set; } = "";
[Parameter] public object? Value { get; set; }
/// <summary>When true, render as button and raise click/mouseleave via context.</summary>
[Parameter] public bool IsInteractive { get; set; }
[CascadingParameter] public EffectiveConfigUiContext? Ui { get; set; }
private Task OnClick()
{
if (Ui == null || Source == null) return Task.CompletedTask;
return Ui.RequestReadonlyPopoverAsync(new ReadonlyBadgeClickedEventArgs(AnchorId, Source, OptionName, Value));
}
private Task OnMouseLeave()
{
if (Ui == null) return Task.CompletedTask;
return Ui.ScheduleReadonlyPopoverCloseAsync();
}
}