Compare commits
7 Commits
7552b34ced
...
6ac48a472d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ac48a472d | ||
|
|
96dfd5b3c6 | ||
|
|
810771f385 | ||
|
|
a0e0d7ed03 | ||
|
|
39cb63a78d | ||
|
|
7d08923444 | ||
|
|
11374347d3 |
@@ -5,10 +5,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
@DxResourceManager.RegisterTheme(Themes.Fluent.Clone(properties =>
|
||||
{
|
||||
properties.ApplyToPageElements = true;
|
||||
}))
|
||||
@DxResourceManager.RegisterTheme(Themes.Fluent)
|
||||
@DxResourceManager.RegisterScripts()
|
||||
|
||||
<link href="_content/DevExpress.Blazor.Dashboard/ace.css" rel="stylesheet" />
|
||||
|
||||
55
DbFirst.BlazorWebApp/Components/BandEditor.razor
Normal file
55
DbFirst.BlazorWebApp/Components/BandEditor.razor
Normal 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" : "")">►</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; }
|
||||
}
|
||||
@@ -21,7 +21,6 @@ public abstract class BandGridBase<TItem> : ComponentBase
|
||||
protected Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||
protected string? layoutUser;
|
||||
protected bool gridLayoutApplied;
|
||||
protected bool bandEditorExpanded;
|
||||
protected IGrid? gridRef;
|
||||
|
||||
// --- SizeMode ---
|
||||
|
||||
@@ -24,44 +24,17 @@ else if (items.Count == 0)
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="band-editor">
|
||||
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
|
||||
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">►</span>
|
||||
<span>Layout</span>
|
||||
</button>
|
||||
@if (bandEditorExpanded)
|
||||
{
|
||||
<div class="band-editor-body">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Layout speichern" Click="SaveLayoutWithFeedbackAsync" Enabled="@CanSaveBandLayout" />
|
||||
<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>
|
||||
<BandEditor Bands="@bandLayout.Bands"
|
||||
BandOptions="@bandOptions"
|
||||
Columns="@ColumnDefinitions"
|
||||
GetColumnBand="GetColumnBand"
|
||||
CanSave="@CanSaveBandLayout"
|
||||
OnAddBand="AddBand"
|
||||
OnSaveLayout="SaveLayoutWithFeedbackAsync"
|
||||
OnResetLayout="ResetLayoutWithFeedbackAsync"
|
||||
OnRemoveBand="RemoveBand"
|
||||
OnBandCaptionChanged="@(args => UpdateBandCaption(args.Band, args.Value))"
|
||||
OnColumnBandChanged="@(args => UpdateColumnBand(args.FieldName, args.BandId))" />
|
||||
|
||||
<div class="grid-section">
|
||||
<DxGrid Data="@items"
|
||||
|
||||
@@ -25,44 +25,17 @@ else if (items.Count == 0)
|
||||
else
|
||||
{
|
||||
|
||||
<div class="band-editor">
|
||||
<button class="band-editor-toggle" @onclick="() => bandEditorExpanded = !bandEditorExpanded">
|
||||
<span class="band-editor-toggle-icon @(bandEditorExpanded ? "expanded" : "")">►</span>
|
||||
<span>Layout</span>
|
||||
</button>
|
||||
@if (bandEditorExpanded)
|
||||
{
|
||||
<div class="band-editor-body">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Layout speichern" Click="SaveLayoutWithFeedbackAsync" Enabled="@CanSaveBandLayout" />
|
||||
<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>
|
||||
<BandEditor Bands="@bandLayout.Bands"
|
||||
BandOptions="@bandOptions"
|
||||
Columns="@ColumnDefinitions"
|
||||
GetColumnBand="GetColumnBand"
|
||||
CanSave="@CanSaveBandLayout"
|
||||
OnAddBand="AddBand"
|
||||
OnSaveLayout="SaveLayoutWithFeedbackAsync"
|
||||
OnResetLayout="ResetLayoutWithFeedbackAsync"
|
||||
OnRemoveBand="RemoveBand"
|
||||
OnBandCaptionChanged="@(args => UpdateBandCaption(args.Band, args.Value))"
|
||||
OnColumnBandChanged="@(args => UpdateColumnBand(args.FieldName, args.BandId))" />
|
||||
|
||||
<div class="mb-3 page-size-selector">
|
||||
<span class="page-size-label">Datensätze je Seite:</span>
|
||||
@@ -332,18 +305,17 @@ else
|
||||
Category = editModel.Category,
|
||||
StatusFlag = editModel.StatusFlag
|
||||
};
|
||||
try
|
||||
|
||||
var result = await Api.UpsertAsync(dto);
|
||||
if (!result.Success)
|
||||
{
|
||||
var saved = await Api.UpsertAsync(dto);
|
||||
infoMessage = editModel.IsNew ? "MassData angelegt." : "MassData aktualisiert.";
|
||||
focusedRowKey = saved.Id;
|
||||
await LoadPage(pageIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Speichern: {ex.Message}";
|
||||
errorMessage = result.Error ?? "Speichern fehlgeschlagen.";
|
||||
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)
|
||||
|
||||
@@ -67,11 +67,6 @@
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (dashboards.Count == 0)
|
||||
{
|
||||
await RefreshDashboards();
|
||||
}
|
||||
|
||||
var requestedId = string.IsNullOrWhiteSpace(DashboardId) || string.Equals(DashboardId, "default", StringComparison.OrdinalIgnoreCase)
|
||||
? null
|
||||
: DashboardId;
|
||||
|
||||
@@ -31,8 +31,8 @@
|
||||
|
||||
public class CarouselData
|
||||
{
|
||||
public string Source { get; set; }
|
||||
public string AlternateText { get; set; }
|
||||
public string Source { get; set; } = string.Empty;
|
||||
public string AlternateText { get; set; } = string.Empty;
|
||||
|
||||
public CarouselData(string source, string alt)
|
||||
{
|
||||
|
||||
60
DbFirst.BlazorWebApp/Services/ApiClientHelper.cs
Normal file
60
DbFirst.BlazorWebApp/Services/ApiClientHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public class CatalogApiClient
|
||||
return ApiResult<CatalogReadDto?>.Ok(payload);
|
||||
}
|
||||
|
||||
var error = await ReadErrorAsync(response);
|
||||
var error = await ApiClientHelper.ReadErrorAsync(response);
|
||||
return ApiResult<CatalogReadDto?>.Fail(error);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ public class CatalogApiClient
|
||||
return ApiResult<bool>.Ok(true);
|
||||
}
|
||||
|
||||
var error = await ReadErrorAsync(response);
|
||||
var error = await ApiClientHelper.ReadErrorAsync(response);
|
||||
return ApiResult<bool>.Fail(error);
|
||||
}
|
||||
|
||||
@@ -58,68 +58,9 @@ public class CatalogApiClient
|
||||
return ApiResult<bool>.Ok(true);
|
||||
}
|
||||
|
||||
var error = await ReadErrorAsync(response);
|
||||
var error = await ApiClientHelper.ReadErrorAsync(response);
|
||||
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)
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
internal sealed class ProblemDetailsDto
|
||||
{
|
||||
public string? Type { get; set; }
|
||||
public string? Title { get; set; }
|
||||
public string? Detail { get; set; }
|
||||
}
|
||||
|
||||
@@ -36,12 +36,17 @@ public class MassDataApiClient
|
||||
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);
|
||||
response.EnsureSuccessStatusCode();
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
var payload = await response.Content.ReadFromJsonAsync<MassDataReadDto>();
|
||||
return payload ?? new MassDataReadDto();
|
||||
return ApiResult<MassDataReadDto?>.Ok(payload);
|
||||
}
|
||||
|
||||
var error = await ApiClientHelper.ReadErrorAsync(response);
|
||||
return ApiResult<MassDataReadDto?>.Fail(error);
|
||||
}
|
||||
|
||||
public async Task<MassDataReadDto?> GetByCustomerNameAsync(string customerName)
|
||||
|
||||
Reference in New Issue
Block a user