feat(client): migrate SearchEdit.razor to ISearchApiClient

- Replace ISearchService with ISearchApiClient
- Add @using for JdeScoping.Core.ApiContracts and JdeScoping.Client.Extensions
- Update LoadSearchAsync to use result.Switch() pattern with ApiResult<T>
- Handle CopySearchId, Id, and new search cases with proper error handling
- Use ToClient() extension method to convert Core to Client SearchViewModel
- Add _errorMessage field and display error alert in UI
- Update SubmitSearchInternalAsync and DownloadResultsAsync for consistency
- Add FormatValidationErrors helper for ValidationError.FieldErrors
This commit is contained in:
Joseph Doherty
2026-01-06 10:23:36 -05:00
parent b86d48657e
commit a77b71e53d
@@ -1,7 +1,9 @@
@page "/search"
@page "/search/{Id:int}"
@attribute [Authorize]
@inject ISearchService SearchService
@using JdeScoping.Core.ApiContracts
@using JdeScoping.Client.Extensions
@inject ISearchApiClient SearchApi
@inject IHubConnectionService HubConnection
@inject IFileService FileService
@inject AuthStateProvider AuthStateProvider
@@ -25,6 +27,12 @@
{
<LoadingIndicator Message="Loading search..." />
}
else if (!string.IsNullOrEmpty(_errorMessage))
{
<RadzenAlert AlertStyle="AlertStyle.Danger" ShowIcon="true" Variant="Variant.Flat" class="rz-mb-4">
@_errorMessage
</RadzenAlert>
}
else
{
<EditForm Model="@_search" OnValidSubmit="@HandleValidSubmit">
@@ -169,6 +177,7 @@ else
private bool _isLoading = true;
private bool _isSubmitting;
private string? _errorMessage;
// Filter visibility flags
private bool _showTimespan;
@@ -190,25 +199,37 @@ else
private async Task LoadSearchAsync()
{
_isLoading = true;
_errorMessage = null;
try
{
if (CopySearchId.HasValue)
{
var copied = await SearchService.CopySearchAsync(CopySearchId.Value);
if (copied != null)
{
_search = copied;
_search.Id = 0;
_search.Status = "New";
}
var result = await SearchApi.CopySearchAsync(CopySearchId.Value);
result.Switch(
copied =>
{
_search = copied.ToClient();
_search.Id = 0;
_search.Status = "New";
},
notFound => { _errorMessage = "Search to copy not found."; },
validation => { _errorMessage = FormatValidationErrors(validation.FieldErrors); },
unauthorized => { _errorMessage = "Session expired."; },
forbidden => { _errorMessage = "Access denied."; },
error => { _errorMessage = error.Message; }
);
}
else if (Id.HasValue && Id.Value > 0)
{
var loaded = await SearchService.GetSearchAsync(Id.Value);
if (loaded != null)
{
_search = loaded;
}
var result = await SearchApi.GetSearchAsync(Id.Value);
result.Switch(
loaded => { _search = loaded.ToClient(); },
notFound => { _errorMessage = "Search not found."; },
validation => { _errorMessage = FormatValidationErrors(validation.FieldErrors); },
unauthorized => { _errorMessage = "Session expired."; },
forbidden => { _errorMessage = "Access denied."; },
error => { _errorMessage = error.Message; }
);
}
else
{
@@ -221,8 +242,11 @@ else
};
}
// Detect search type from criteria
DetectSearchType();
// Detect search type from criteria (only if no error)
if (string.IsNullOrEmpty(_errorMessage))
{
DetectSearchType();
}
}
finally
{
@@ -230,6 +254,12 @@ else
}
}
private static string FormatValidationErrors(IReadOnlyDictionary<string, string[]> fieldErrors)
{
var messages = fieldErrors.SelectMany(kv => kv.Value);
return string.Join(" ", messages);
}
private void DetectSearchType()
{
var criteria = _search.Criteria;
@@ -364,15 +394,15 @@ else
_isSubmitting = true;
try
{
var id = await SearchService.SaveSearchAsync(_search);
if (id.HasValue)
{
NavigationManager.NavigateTo($"/search/{id}");
}
else
{
NotificationService.Notify(NotificationSeverity.Error, "Error", "Failed to submit search.");
}
var result = await SearchApi.CreateSearchAsync(_search.ToCore());
result.Switch(
id => { NavigationManager.NavigateTo($"/search/{id}"); },
notFound => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Search not found."); },
validation => { NotificationService.Notify(NotificationSeverity.Error, "Validation Error", FormatValidationErrors(validation.FieldErrors)); },
unauthorized => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
forbidden => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
error => { NotificationService.Notify(NotificationSeverity.Error, "Error", error.Message); }
);
}
finally
{
@@ -413,17 +443,26 @@ else
private async Task DownloadResultsAsync()
{
var results = await SearchService.DownloadResultsAsync(_search.Id);
if (results != null && results.Length > 0)
{
// Trigger download via JS interop
await JSRuntime.InvokeVoidAsync("downloadFile", $"search_results_{_search.Id}.xlsx", results);
NotificationService.Notify(NotificationSeverity.Success, "Download", "Results downloaded successfully.");
}
else
{
NotificationService.Notify(NotificationSeverity.Warning, "Download", "No results available to download.");
}
var result = await SearchApi.GetResultsAsync(_search.Id);
result.Switch(
bytes =>
{
if (bytes.Length > 0)
{
_ = JSRuntime.InvokeVoidAsync("downloadFile", $"search_results_{_search.Id}.xlsx", bytes);
NotificationService.Notify(NotificationSeverity.Success, "Download", "Results downloaded successfully.");
}
else
{
NotificationService.Notify(NotificationSeverity.Warning, "Download", "No results available to download.");
}
},
notFound => { NotificationService.Notify(NotificationSeverity.Warning, "Download", "Results not found."); },
validation => { NotificationService.Notify(NotificationSeverity.Error, "Error", FormatValidationErrors(validation.FieldErrors)); },
unauthorized => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
forbidden => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
error => { NotificationService.Notify(NotificationSeverity.Error, "Error", error.Message); }
);
}
public void Dispose()