Compare commits

...

7 Commits

Author SHA1 Message Date
OlgunR
6ac48a472d Simplify Fluent theme registration in App.razor
Replaced custom Fluent theme registration (with ApplyToPageElements set to true) with the default Fluent theme registration, removing unnecessary property customization for a cleaner setup.
2026-04-15 14:16:35 +02:00
OlgunR
96dfd5b3c6 Set default values for CarouselData string properties
Initialized Source and AlternateText with empty strings in the
CarouselData class to prevent potential null reference issues.
2026-04-15 14:16:03 +02:00
OlgunR
810771f385 Remove auto-refresh of dashboards in OnParametersSetAsync
Previously, RefreshDashboards() was called automatically if the dashboards list was empty during parameter setting. This logic has been removed, so dashboards will no longer refresh automatically in this scenario.
2026-04-15 14:08:14 +02:00
OlgunR
a0e0d7ed03 Centralize API error handling in ApiClientHelper
Refactored error handling logic for API responses into a new static ApiClientHelper class, consolidating the ReadErrorAsync method and ProblemDetailsDto. Updated CatalogApiClient and MassDataApiClient to use the shared helper, removing redundant local methods and classes. This change improves consistency, reduces duplication, and streamlines error message formatting across the application.
2026-04-15 13:55:57 +02:00
OlgunR
39cb63a78d Refactor UpsertAsync to use ApiResult for error handling
MassDataApiClient.UpsertAsync now returns ApiResult to standardize success and error reporting, including detailed error extraction from API responses. Updated MassDataGrid.razor to handle the new result type and display error messages accordingly. Removed obsolete try-catch logic in favor of the new pattern.
2026-04-15 11:27:48 +02:00
OlgunR
7d08923444 Remove unused bandEditorExpanded field from BandGridBase
The protected bool bandEditorExpanded, which tracked the expansion
state of the band editor, has been removed from BandGridBase<TItem>
as it is no longer used. This helps clean up unused state from
the component.
2026-04-15 11:01:22 +02:00
OlgunR
11374347d3 Refactor band editor into reusable BandEditor component
Extract band editor UI and logic from CatalogsGrid.razor and MassDataGrid.razor into a new BandEditor.razor component. This centralizes band management features (add/remove bands, edit captions, assign columns, save/reset layout) and reduces code duplication, improving maintainability and reusability. Existing UI and event handling remain functionally unchanged.
2026-04-15 10:57:28 +02:00
10 changed files with 160 additions and 169 deletions

View File

@@ -5,10 +5,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" /> <base href="/" />
@DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties => @DxResourceManager.RegisterTheme(Themes.Fluent)
{
properties.ApplyToPageElements = true;
}))
@DxResourceManager.RegisterScripts() @DxResourceManager.RegisterScripts()
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" /> <link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />

View File

@@ -0,0 +1,55 @@
<div class="band-editor">
<button class="band-editor-toggle" @onclick="() => IsExpanded = !IsExpanded">
<span class="band-editor-toggle-icon @(IsExpanded ? "expanded" : "")">&#9658;</span>
<span>Layout</span>
</button>
@if (IsExpanded)
{
<div class="band-editor-body">
<div class="band-controls">
<DxButton Text="Band hinzufügen" Click="OnAddBand" />
<DxButton Text="Layout speichern" Click="OnSaveLayout" Enabled="@CanSave" />
<DxButton Text="Layout zurücksetzen" Click="OnResetLayout" />
</div>
@foreach (var band in Bands)
{
<div class="band-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => OnBandCaptionChanged.InvokeAsync((band, value)))" />
<DxButton Text="Entfernen" Click="@(() => OnRemoveBand.InvokeAsync(band))" />
</div>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in Columns)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@BandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => OnColumnBandChanged.InvokeAsync((column.FieldName, value)))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
}
</div>
@code {
private bool IsExpanded { get; set; }
[Parameter, EditorRequired] public List<BandDefinition> Bands { get; set; } = new();
[Parameter, EditorRequired] public List<BandOption> BandOptions { get; set; } = new();
[Parameter, EditorRequired] public List<ColumnDefinition> Columns { get; set; } = new();
[Parameter, EditorRequired] public Func<string, string> GetColumnBand { get; set; } = _ => string.Empty;
[Parameter, EditorRequired] public bool CanSave { get; set; }
[Parameter] public EventCallback OnAddBand { get; set; }
[Parameter] public EventCallback OnSaveLayout { get; set; }
[Parameter] public EventCallback OnResetLayout { get; set; }
[Parameter] public EventCallback<BandDefinition> OnRemoveBand { get; set; }
[Parameter] public EventCallback<(BandDefinition Band, string Value)> OnBandCaptionChanged { get; set; }
[Parameter] public EventCallback<(string FieldName, string? BandId)> OnColumnBandChanged { get; set; }
}

View File

@@ -21,7 +21,6 @@ public abstract class BandGridBase<TItem> : ComponentBase
protected Dictionary<string, ColumnDefinition> columnLookup = new(); protected Dictionary<string, ColumnDefinition> columnLookup = new();
protected string? layoutUser; protected string? layoutUser;
protected bool gridLayoutApplied; protected bool gridLayoutApplied;
protected bool bandEditorExpanded;
protected IGrid? gridRef; protected IGrid? gridRef;
// --- SizeMode --- // --- SizeMode ---

View File

@@ -24,44 +24,17 @@ else if (items.Count == 0)
} }
else else
{ {
<div class="band-editor"> <BandEditor Bands="@bandLayout.Bands"
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded"> BandOptions="@bandOptions"
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">&#9658;</span> Columns="@ColumnDefinitions"
<span>Layout</span> GetColumnBand="GetColumnBand"
</button> CanSave="@CanSaveBandLayout"
@if (bandEditorExpanded) OnAddBand="AddBand"
{ OnSaveLayout="SaveLayoutWithFeedbackAsync"
<div class="band-editor-body"> OnResetLayout="ResetLayoutWithFeedbackAsync"
<div class="band-controls"> OnRemoveBand="RemoveBand"
<DxButton Text="Band hinzufügen" Click="AddBand" /> OnBandCaptionChanged="@(args => UpdateBandCaption(args.Band, args.Value))"
<DxButton Text="Layout speichern" Click="SaveLayoutWithFeedbackAsync" Enabled="@CanSaveBandLayout" /> OnColumnBandChanged="@(args => UpdateColumnBand(args.FieldName, args.BandId))" />
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutWithFeedbackAsync" />
</div>
@foreach (var band in bandLayout.Bands)
{
<div class="band-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
</div>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in ColumnDefinitions)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@bandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
}
</div>
<div class="grid-section"> <div class="grid-section">
<DxGrid Data="@items" <DxGrid Data="@items"

View File

@@ -25,44 +25,17 @@ else if (items.Count == 0)
else else
{ {
<div class="band-editor"> <BandEditor Bands="@bandLayout.Bands"
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded"> BandOptions="@bandOptions"
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">&#9658;</span> Columns="@ColumnDefinitions"
<span>Layout</span> GetColumnBand="GetColumnBand"
</button> CanSave="@CanSaveBandLayout"
@if (bandEditorExpanded) OnAddBand="AddBand"
{ OnSaveLayout="SaveLayoutWithFeedbackAsync"
<div class="band-editor-body"> OnResetLayout="ResetLayoutWithFeedbackAsync"
<div class="band-controls"> OnRemoveBand="RemoveBand"
<DxButton Text="Band hinzufügen" Click="AddBand" /> OnBandCaptionChanged="@(args => UpdateBandCaption(args.Band, args.Value))"
<DxButton Text="Layout speichern" Click="SaveLayoutWithFeedbackAsync" Enabled="@CanSaveBandLayout" /> OnColumnBandChanged="@(args => UpdateColumnBand(args.FieldName, args.BandId))" />
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutWithFeedbackAsync" />
</div>
@foreach (var band in bandLayout.Bands)
{
<div class="band-row">
<DxTextBox Text="@band.Caption" TextChanged="@(value => UpdateBandCaption(band, value))" />
<DxButton Text="Entfernen" Click="@(() => RemoveBand(band))" />
</div>
}
<DxFormLayout CssClass="band-columns" ColCount="2">
@foreach (var column in ColumnDefinitions)
{
<DxFormLayoutItem Caption="@column.Caption">
<DxComboBox Data="@bandOptions"
TData="BandOption"
TValue="string"
TextFieldName="Caption"
ValueFieldName="Id"
Value="@GetColumnBand(column.FieldName)"
ValueChanged="@(value => UpdateColumnBand(column.FieldName, value))"
Width="100%" />
</DxFormLayoutItem>
}
</DxFormLayout>
</div>
}
</div>
<div class="mb-3 page-size-selector"> <div class="mb-3 page-size-selector">
<span class="page-size-label">Datensätze je Seite:</span> <span class="page-size-label">Datensätze je Seite:</span>
@@ -332,18 +305,17 @@ else
Category = editModel.Category, Category = editModel.Category,
StatusFlag = editModel.StatusFlag StatusFlag = editModel.StatusFlag
}; };
try
var result = await Api.UpsertAsync(dto);
if (!result.Success)
{ {
var saved = await Api.UpsertAsync(dto); errorMessage = result.Error ?? "Speichern fehlgeschlagen.";
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
focusedRowKey = saved.Id;
await LoadPage(pageIndex);
}
catch (Exception ex)
{
errorMessage = $"Fehler beim Speichern: {ex.Message}";
e.Cancel = true; e.Cancel = true;
return;
} }
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
focusedRowKey = result.Value?.Id;
await LoadPage(pageIndex);
} }
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message) private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)

View File

@@ -67,11 +67,6 @@
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
if (dashboards.Count == 0)
{
await RefreshDashboards();
}
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase) var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
? null ? null
: DashboardId; : DashboardId;

View File

@@ -31,8 +31,8 @@
public class CarouselData public class CarouselData
{ {
public string Source { get; set; } public string Source { get; set; } = string.Empty;
public string AlternateText { get; set; } public string AlternateText { get; set; } = string.Empty;
public CarouselData(string source, string alt) public CarouselData(string source, string alt)
{ {

View File

@@ -0,0 +1,60 @@
using System.Net;
using System.Net.Http.Json;
namespace DbFirst.BlazorWebApp.Services;
internal sealed class ProblemDetailsDto
{
public string? Type { get; set; }
public string? Title { get; set; }
public string? Detail { get; set; }
}
internal static class ApiClientHelper
{
public static async Task<string> ReadErrorAsync(HttpResponseMessage response)
{
string? problemTitle = null;
string? problemDetail = null;
try
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetailsDto>();
if (problem != null)
{
problemTitle = problem.Title;
problemDetail = problem.Detail ?? problem.Type;
}
}
catch { }
var status = response.StatusCode;
var reason = response.ReasonPhrase;
var body = await response.Content.ReadAsStringAsync();
string? detail = problemDetail;
if (string.IsNullOrWhiteSpace(detail) && !string.IsNullOrWhiteSpace(body))
detail = body;
return status switch
{
HttpStatusCode.BadRequest => $"Eingabe ungültig{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.NotFound => $"Nicht gefunden{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Conflict => $"Konflikt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Unauthorized => $"Nicht autorisiert{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Forbidden => $"Nicht erlaubt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.InternalServerError => $"Serverfehler{FormatSuffix(problemTitle, detail, reason)}",
_ => $"Fehler {(int)status} {reason ?? string.Empty}{FormatSuffix(problemTitle, detail, reason)}"
};
}
private static string FormatSuffix(string? title, string? detail, string? reason)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(title)) parts.Add(title);
if (!string.IsNullOrWhiteSpace(detail)) parts.Add(detail);
if (parts.Count == 0 && !string.IsNullOrWhiteSpace(reason)) parts.Add(reason);
if (parts.Count == 0) return string.Empty;
return ": " + string.Join(" | ", parts);
}
}

View File

@@ -34,7 +34,7 @@ public class CatalogApiClient
return ApiResult<CatalogReadDto?>.Ok(payload); return ApiResult<CatalogReadDto?>.Ok(payload);
} }
var error = await ReadErrorAsync(response); var error = await ApiClientHelper.ReadErrorAsync(response);
return ApiResult<CatalogReadDto?>.Fail(error); return ApiResult<CatalogReadDto?>.Fail(error);
} }
@@ -46,7 +46,7 @@ public class CatalogApiClient
return ApiResult<bool>.Ok(true); return ApiResult<bool>.Ok(true);
} }
var error = await ReadErrorAsync(response); var error = await ApiClientHelper.ReadErrorAsync(response);
return ApiResult<bool>.Fail(error); return ApiResult<bool>.Fail(error);
} }
@@ -58,68 +58,9 @@ public class CatalogApiClient
return ApiResult<bool>.Ok(true); return ApiResult<bool>.Ok(true);
} }
var error = await ReadErrorAsync(response); var error = await ApiClientHelper.ReadErrorAsync(response);
return ApiResult<bool>.Fail(error); return ApiResult<bool>.Fail(error);
} }
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
{
string? problemTitle = null;
string? problemDetail = null;
try
{
var problem = await response.Content.ReadFromJsonAsync<ProblemDetailsDto>();
if (problem != null)
{
problemTitle = problem.Title;
problemDetail = problem.Detail ?? problem.Type;
}
}
catch
{
}
var status = response.StatusCode;
var reason = response.ReasonPhrase;
var body = await response.Content.ReadAsStringAsync();
string? detail = problemDetail;
if (string.IsNullOrWhiteSpace(detail) && !string.IsNullOrWhiteSpace(body))
{
detail = body;
}
if (status == HttpStatusCode.Conflict)
{
return "Datensatz existiert bereits. Bitte wählen Sie einen anderen Titel.";
}
if (status == HttpStatusCode.BadRequest && (detail?.Contains("CatTitle cannot be changed", StringComparison.OrdinalIgnoreCase) ?? false))
{
return "Titel kann nicht geändert werden.";
}
return status switch
{
HttpStatusCode.BadRequest => $"Eingabe ungültig{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.NotFound => $"Nicht gefunden{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Conflict => $"Konflikt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Unauthorized => $"Nicht autorisiert{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.Forbidden => $"Nicht erlaubt{FormatSuffix(problemTitle, detail, reason)}",
HttpStatusCode.InternalServerError => $"Serverfehler{FormatSuffix(problemTitle, detail, reason)}",
_ => $"Fehler {(int)status} {reason ?? string.Empty}{FormatSuffix(problemTitle, detail, reason)}"
};
}
private static string FormatSuffix(string? title, string? detail, string? reason)
{
var parts = new List<string>();
if (!string.IsNullOrWhiteSpace(title)) parts.Add(title);
if (!string.IsNullOrWhiteSpace(detail)) parts.Add(detail);
if (parts.Count == 0 && !string.IsNullOrWhiteSpace(reason)) parts.Add(reason);
if (parts.Count == 0) return string.Empty;
return ": " + string.Join(" | ", parts);
}
} }
public record ApiResult<T>(bool Success, T? Value, string? Error) public record ApiResult<T>(bool Success, T? Value, string? Error)
@@ -128,9 +69,3 @@ public record ApiResult<T>(bool Success, T? Value, string? Error)
public static ApiResult<T> Fail(string? error) => new(false, default, error); public static ApiResult<T> Fail(string? error) => new(false, default, error);
} }
internal sealed class ProblemDetailsDto
{
public string? Type { get; set; }
public string? Title { get; set; }
public string? Detail { get; set; }
}

View File

@@ -36,12 +36,17 @@ public class MassDataApiClient
return result ?? new List<MassDataReadDto>(); return result ?? new List<MassDataReadDto>();
} }
public async Task<MassDataReadDto> UpsertAsync(MassDataWriteDto dto) public async Task<ApiResult<MassDataReadDto?>> UpsertAsync(MassDataWriteDto dto)
{ {
var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto); var response = await _httpClient.PostAsJsonAsync($"{Endpoint}/upsert", dto);
response.EnsureSuccessStatusCode(); if (response.IsSuccessStatusCode)
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>(); {
return payload ?? new MassDataReadDto(); var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
return ApiResult<MassDataReadDto?>.Ok(payload);
}
var error = await ApiClientHelper.ReadErrorAsync(response);
return ApiResult<MassDataReadDto?>.Fail(error);
} }
public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName) public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName)