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"
@page "/search/{Id:int}" @page "/search/{Id:int}"
@attribute [Authorize] @attribute [Authorize]
@inject ISearchService SearchService @using JdeScoping.Core.ApiContracts
@using JdeScoping.Client.Extensions
@inject ISearchApiClient SearchApi
@inject IHubConnectionService HubConnection @inject IHubConnectionService HubConnection
@inject IFileService FileService @inject IFileService FileService
@inject AuthStateProvider AuthStateProvider @inject AuthStateProvider AuthStateProvider
@@ -25,6 +27,12 @@
{ {
<LoadingIndicator Message="Loading search..." /> <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 else
{ {
<EditForm Model="@_search" OnValidSubmit="@HandleValidSubmit"> <EditForm Model="@_search" OnValidSubmit="@HandleValidSubmit">
@@ -169,6 +177,7 @@ else
private bool _isLoading = true; private bool _isLoading = true;
private bool _isSubmitting; private bool _isSubmitting;
private string? _errorMessage;
// Filter visibility flags // Filter visibility flags
private bool _showTimespan; private bool _showTimespan;
@@ -190,25 +199,37 @@ else
private async Task LoadSearchAsync() private async Task LoadSearchAsync()
{ {
_isLoading = true; _isLoading = true;
_errorMessage = null;
try try
{ {
if (CopySearchId.HasValue) if (CopySearchId.HasValue)
{ {
var copied = await SearchService.CopySearchAsync(CopySearchId.Value); var result = await SearchApi.CopySearchAsync(CopySearchId.Value);
if (copied != null) result.Switch(
{ copied =>
_search = copied; {
_search.Id = 0; _search = copied.ToClient();
_search.Status = "New"; _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) else if (Id.HasValue && Id.Value > 0)
{ {
var loaded = await SearchService.GetSearchAsync(Id.Value); var result = await SearchApi.GetSearchAsync(Id.Value);
if (loaded != null) result.Switch(
{ loaded => { _search = loaded.ToClient(); },
_search = loaded; notFound => { _errorMessage = "Search not found."; },
} validation => { _errorMessage = FormatValidationErrors(validation.FieldErrors); },
unauthorized => { _errorMessage = "Session expired."; },
forbidden => { _errorMessage = "Access denied."; },
error => { _errorMessage = error.Message; }
);
} }
else else
{ {
@@ -221,8 +242,11 @@ else
}; };
} }
// Detect search type from criteria // Detect search type from criteria (only if no error)
DetectSearchType(); if (string.IsNullOrEmpty(_errorMessage))
{
DetectSearchType();
}
} }
finally 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() private void DetectSearchType()
{ {
var criteria = _search.Criteria; var criteria = _search.Criteria;
@@ -364,15 +394,15 @@ else
_isSubmitting = true; _isSubmitting = true;
try try
{ {
var id = await SearchService.SaveSearchAsync(_search); var result = await SearchApi.CreateSearchAsync(_search.ToCore());
if (id.HasValue) result.Switch(
{ id => { NavigationManager.NavigateTo($"/search/{id}"); },
NavigationManager.NavigateTo($"/search/{id}"); notFound => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Search not found."); },
} validation => { NotificationService.Notify(NotificationSeverity.Error, "Validation Error", FormatValidationErrors(validation.FieldErrors)); },
else unauthorized => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Session expired."); },
{ forbidden => { NotificationService.Notify(NotificationSeverity.Error, "Error", "Access denied."); },
NotificationService.Notify(NotificationSeverity.Error, "Error", "Failed to submit search."); error => { NotificationService.Notify(NotificationSeverity.Error, "Error", error.Message); }
} );
} }
finally finally
{ {
@@ -413,17 +443,26 @@ else
private async Task DownloadResultsAsync() private async Task DownloadResultsAsync()
{ {
var results = await SearchService.DownloadResultsAsync(_search.Id); var result = await SearchApi.GetResultsAsync(_search.Id);
if (results != null && results.Length > 0) result.Switch(
{ bytes =>
// Trigger download via JS interop {
await JSRuntime.InvokeVoidAsync("downloadFile", $"search_results_{_search.Id}.xlsx", results); if (bytes.Length > 0)
NotificationService.Notify(NotificationSeverity.Success, "Download", "Results downloaded successfully."); {
} _ = JSRuntime.InvokeVoidAsync("downloadFile", $"search_results_{_search.Id}.xlsx", bytes);
else NotificationService.Notify(NotificationSeverity.Success, "Download", "Results downloaded successfully.");
{ }
NotificationService.Notify(NotificationSeverity.Warning, "Download", "No results available to download."); 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() public void Dispose()