Effective config: editable badge in view mode, refresh on continue/stop after save failure

- Multi-value items: EditableAsButton=true so 'editable' badge is clickable in view
  mode and activates the field like other editable badges.
- Save modal: 'Continue editing' after ValidateFailed/ReloadFailed now discards
  session, refreshes config from disk, then re-enters edit mode so remove/UI
  work correctly.
- 'Stop editing' after ValidateFailed/ReloadFailed now triggers config refresh
  so the invalid value on disk is shown instead of stale UI.
- SaveFlow: HandleCloseAndEndEdit invokes OnSaveCompleted when in failed state;
  OnContinueEditing uses EventCallback.Factory.Create to fix method-group
  conversion warning.
This commit is contained in:
2026-03-07 22:13:31 +10:00
parent ff94be87b2
commit f2e73107d1
4 changed files with 41 additions and 4 deletions
@@ -28,7 +28,7 @@
</CascadingValue> </CascadingValue>
</CascadingValue> </CascadingValue>
<EffectiveConfigSaveFlow ShowModal="@_showSaveModal" PendingChanges="@Session.PendingChanges" Status="@Status" Session="@Session" <EffectiveConfigSaveFlow ShowModal="@_showSaveModal" PendingChanges="@Session.PendingChanges" Status="@Status" Session="@Session"
OnClose="CloseSaveModal" OnSaveCompleted="OnSaveCompleted" OnCloseAndEndEdit="CloseSaveModalAndEndEdit" /> OnClose="CloseSaveModal" OnSaveCompleted="OnSaveCompleted" OnContinueEditingAfterSavedFailure="ContinueEditingAfterSavedFailureAsync" OnCloseAndEndEdit="CloseSaveModalAndEndEdit" />
</div> </div>
} }
@@ -212,6 +212,15 @@
StateHasChanged(); StateHasChanged();
} }
private async Task ContinueEditingAfterSavedFailureAsync()
{
_showSaveModal = false;
Session.ExitEditModeDiscard();
await OnSaveCompleted.InvokeAsync();
Session.EnterEditMode();
StateHasChanged();
}
private void CloseSaveModalAndEndEdit() private void CloseSaveModalAndEndEdit()
{ {
_showSaveModal = false; _showSaveModal = false;
@@ -6,7 +6,7 @@
@Value @Value
<OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@OptionName" Value="@Value" <OptionStateBadge AnchorId="@(ReadonlyBadgeAnchorId ?? "")" Source="@Source" OptionName="@OptionName" Value="@Value"
IsInteractive="@(IsEditMode && !ReadonlyClickActivatesField && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))" IsInteractive="@(IsEditMode && !ReadonlyClickActivatesField && !string.IsNullOrEmpty(ReadonlyBadgeAnchorId))"
ShowEditableBadge="@(Source?.IsReadOnly != true)" EditableAsButton="@IsEditMode" ShowEditableBadge="@(Source?.IsReadOnly != true)" EditableAsButton="true"
FieldKey="@FieldKey" ReadonlyClickActivatesField="@ReadonlyClickActivatesField" ReadonlyTitleOverride="@ReadonlyTitleOverride" /> FieldKey="@FieldKey" ReadonlyClickActivatesField="@ReadonlyClickActivatesField" ReadonlyTitleOverride="@ReadonlyTitleOverride" />
</span> </span>
@@ -16,10 +16,11 @@
SaveResult="@_saveResult" SaveResult="@_saveResult"
RestoreResult="@_restoreResult" RestoreResult="@_restoreResult"
OnClose="@OnClose" OnClose="@OnClose"
OnContinueEditing="@(EventCallback.Factory.Create(this, HandleContinueEditing))"
OnSave="@HandleSave" OnSave="@HandleSave"
OnRestore="@HandleRestore" OnRestore="@HandleRestore"
OnReverted="@HandleReverted" OnReverted="@HandleReverted"
OnCloseAndEndEdit="@OnCloseAndEndEdit" /> OnCloseAndEndEdit="@HandleCloseAndEndEdit" />
} }
@code { @code {
@@ -39,6 +40,7 @@
[Parameter] public DnsmasqServiceStatus? Status { get; set; } [Parameter] public DnsmasqServiceStatus? Status { get; set; }
[Parameter] public EventCallback OnClose { get; set; } [Parameter] public EventCallback OnClose { get; set; }
[Parameter] public EventCallback OnSaveCompleted { get; set; } [Parameter] public EventCallback OnSaveCompleted { get; set; }
[Parameter] public EventCallback OnContinueEditingAfterSavedFailure { get; set; }
[Parameter] public EventCallback OnCloseAndEndEdit { get; set; } [Parameter] public EventCallback OnCloseAndEndEdit { get; set; }
[Parameter] public IEffectiveConfigEditSession Session { get; set; } = null!; [Parameter] public IEffectiveConfigEditSession Session { get; set; } = null!;
@@ -127,4 +129,23 @@
_saveState = SaveUiState.RestoreFailed; _saveState = SaveUiState.RestoreFailed;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
private async Task HandleContinueEditing()
{
if (_saveState == SaveUiState.ValidateFailed || _saveState == SaveUiState.ReloadFailed)
{
await OnContinueEditingAfterSavedFailure.InvokeAsync();
return;
}
await OnClose.InvokeAsync();
}
private async Task HandleCloseAndEndEdit()
{
await OnCloseAndEndEdit.InvokeAsync();
if (_saveState == SaveUiState.ValidateFailed || _saveState == SaveUiState.ReloadFailed)
await OnSaveCompleted.InvokeAsync();
}
} }
@@ -153,7 +153,7 @@
@if ((SaveState == EffectiveConfigSaveFlow.SaveUiState.ReloadFailed || SaveState == EffectiveConfigSaveFlow.SaveUiState.ValidateFailed) && SaveResult?.BackupPath != null) @if ((SaveState == EffectiveConfigSaveFlow.SaveUiState.ReloadFailed || SaveState == EffectiveConfigSaveFlow.SaveUiState.ValidateFailed) && SaveResult?.BackupPath != null)
{ {
<button type="button" class="btn btn-primary" @onclick="() => Restore(SaveResult.BackupPath)"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Restore backup</button> <button type="button" class="btn btn-primary" @onclick="() => Restore(SaveResult.BackupPath)"><i class="bi bi-arrow-counterclockwise me-1" aria-hidden="true"></i>Restore backup</button>
<button type="button" class="btn btn-outline-warning" @onclick="Close"><i class="bi bi-pencil me-1" aria-hidden="true"></i>Continue editing</button> <button type="button" class="btn btn-outline-warning" @onclick="ContinueEditing"><i class="bi bi-pencil me-1" aria-hidden="true"></i>Continue editing</button>
<button type="button" class="btn btn-danger" @onclick="CloseAndEndEdit"><i class="bi bi-x-circle me-1" aria-hidden="true"></i>Stop editing</button> <button type="button" class="btn btn-danger" @onclick="CloseAndEndEdit"><i class="bi bi-x-circle me-1" aria-hidden="true"></i>Stop editing</button>
} }
else if (SaveState == EffectiveConfigSaveFlow.SaveUiState.RestoreFailed) else if (SaveState == EffectiveConfigSaveFlow.SaveUiState.RestoreFailed)
@@ -184,6 +184,7 @@
[Parameter] public EffectiveConfigSaveResult? SaveResult { get; set; } [Parameter] public EffectiveConfigSaveResult? SaveResult { get; set; }
[Parameter] public EffectiveConfigRestoreResult? RestoreResult { get; set; } [Parameter] public EffectiveConfigRestoreResult? RestoreResult { get; set; }
[Parameter] public EventCallback OnClose { get; set; } [Parameter] public EventCallback OnClose { get; set; }
[Parameter] public EventCallback OnContinueEditing { get; set; }
[Parameter] public EventCallback OnSave { get; set; } [Parameter] public EventCallback OnSave { get; set; }
[Parameter] public EventCallback<string> OnRestore { get; set; } [Parameter] public EventCallback<string> OnRestore { get; set; }
[Parameter] public EventCallback OnReverted { get; set; } [Parameter] public EventCallback OnReverted { get; set; }
@@ -262,6 +263,12 @@
await OnClose.InvokeAsync(); await OnClose.InvokeAsync();
} }
private async Task ContinueEditing()
{
if (!AllowClose) return;
await OnContinueEditing.InvokeAsync();
}
private async Task Save() private async Task Save()
{ {
await OnSave.InvokeAsync(); await OnSave.InvokeAsync();