Compare commits
8 Commits
c5ca9f0048
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ac8e94334 | ||
|
|
dc74d21426 | ||
|
|
566c3b3276 | ||
|
|
ac84866abe | ||
|
|
d9ce4a5dca | ||
|
|
13617dde87 | ||
|
|
789066a214 | ||
|
|
964d508630 |
@@ -1,60 +1,5 @@
|
||||
@using System.Text.Json
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Rendering
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.Data.Filtering
|
||||
@inject CatalogApiClient Api
|
||||
@inject LayoutApiClient LayoutApi
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
.grid-section { margin-top: 12px; }
|
||||
.catalog-edit-popup {
|
||||
min-width: 720px;
|
||||
}
|
||||
.band-editor {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.band-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.band-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.band-columns {
|
||||
max-width: 720px;
|
||||
}
|
||||
.filter-row-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.filter-operator {
|
||||
width: 52px;
|
||||
min-width: 52px;
|
||||
flex: 0 0 52px;
|
||||
}
|
||||
.filter-value {
|
||||
min-width: 160px;
|
||||
flex: 1 1 160px;
|
||||
}
|
||||
.loading-container {
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@inject BandLayoutService BandLayoutService
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
{
|
||||
@@ -75,15 +20,15 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
|
||||
}
|
||||
else if (items.Count == 0)
|
||||
{
|
||||
<p>Keine Einträge vorhanden.</p>
|
||||
<p>Keine Einträge vorhanden.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="band-editor">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
|
||||
</div>
|
||||
@foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
@@ -155,7 +100,9 @@ else
|
||||
@RenderColumns()
|
||||
</Columns>
|
||||
<EditFormTemplate Context="editFormContext">
|
||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
||||
@{
|
||||
SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew);
|
||||
}
|
||||
<DxFormLayout ColCount="2">
|
||||
<DxFormLayoutItem Caption="Titel">
|
||||
<DxTextBox @bind-Text="editModel.CatTitle" Width="100%" />
|
||||
@@ -197,13 +144,13 @@ else
|
||||
private string popupHeaderText = "Edit";
|
||||
private const string LayoutType = "GRID_BANDS";
|
||||
private const string LayoutKey = "CatalogsGrid";
|
||||
private const string LayoutUserStorageKey = "layoutUser";
|
||||
private string? layoutUser;
|
||||
private BandLayout bandLayout = new();
|
||||
private Dictionary<string, string> columnBandAssignments = new();
|
||||
private List<BandOption> bandOptions = new();
|
||||
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
private bool gridLayoutApplied;
|
||||
|
||||
private List<ColumnDefinition> columnDefinitions = new()
|
||||
{
|
||||
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
|
||||
@@ -211,8 +158,8 @@ else
|
||||
new() { FieldName = nameof(CatalogReadDto.CatString), Caption = "String", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(CatalogReadDto.AddedWho), Caption = "Angelegt von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(CatalogReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
||||
new() { FieldName = nameof(CatalogReadDto.ChangedWho), Caption = "Geändert von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(CatalogReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
||||
new() { FieldName = nameof(CatalogReadDto.ChangedWho), Caption = "Geändert von", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(CatalogReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
||||
};
|
||||
|
||||
private readonly List<ProcedureOption> procedureOptions = new()
|
||||
@@ -222,30 +169,32 @@ else
|
||||
};
|
||||
|
||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||
private bool gridLayoutApplied;
|
||||
|
||||
private DevExpress.Blazor.SizeMode _sizeMode = DevExpress.Blazor.SizeMode.Medium;
|
||||
private static readonly List<DevExpress.Blazor.SizeMode> _sizeModes =
|
||||
Enum.GetValues<DevExpress.Blazor.SizeMode>().ToList();
|
||||
private SizeMode _sizeMode = SizeMode.Medium;
|
||||
private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
|
||||
|
||||
private string FormatSizeText(DevExpress.Blazor.SizeMode size) => size switch
|
||||
private string FormatSizeText(SizeMode size) => size switch
|
||||
{
|
||||
DevExpress.Blazor.SizeMode.Small => "Klein",
|
||||
DevExpress.Blazor.SizeMode.Medium => "Mittel",
|
||||
DevExpress.Blazor.SizeMode.Large => "Groß",
|
||||
SizeMode.Small => "Klein",
|
||||
SizeMode.Medium => "Mittel",
|
||||
SizeMode.Large => "Groß",
|
||||
_ => size.ToString()
|
||||
};
|
||||
|
||||
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
|
||||
{
|
||||
_sizeMode = Enum.Parse<DevExpress.Blazor.SizeMode>(args.ItemInfo.Id);
|
||||
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
await EnsureLayoutUserAsync();
|
||||
await LoadBandLayoutAsync();
|
||||
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
|
||||
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
|
||||
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
|
||||
ApplyColumnLayoutFromStorage();
|
||||
_sizeMode = bandLayout.SizeMode;
|
||||
UpdateBandOptions();
|
||||
await LoadCatalogs();
|
||||
}
|
||||
|
||||
@@ -259,71 +208,6 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void SetEditContext(EditContext context)
|
||||
{
|
||||
if (editContext == context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (editContext != null)
|
||||
{
|
||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
||||
}
|
||||
|
||||
editContext = context;
|
||||
validationMessageStore = new ValidationMessageStore(editContext);
|
||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
||||
}
|
||||
|
||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||
{
|
||||
if (validationMessageStore == null || editContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.UpdateProcedure))
|
||||
{
|
||||
validationMessageStore.Clear();
|
||||
editContext.NotifyValidationStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.CatTitle))
|
||||
{
|
||||
var field = new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle));
|
||||
validationMessageStore.Clear(field);
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPopupHeaderText(bool isNew)
|
||||
{
|
||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
}
|
||||
|
||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
{
|
||||
popupHeaderText = e.IsNew ? "Neu" : "Edit";
|
||||
if (e.IsNew)
|
||||
{
|
||||
e.EditModel = new CatalogEditModel { IsNew = true };
|
||||
return;
|
||||
}
|
||||
|
||||
var item = (CatalogReadDto)e.DataItem;
|
||||
e.EditModel = new CatalogEditModel
|
||||
{
|
||||
Guid = item.Guid,
|
||||
CatTitle = item.CatTitle,
|
||||
CatString = item.CatString,
|
||||
UpdateProcedure = 0,
|
||||
OriginalCatTitle = item.CatTitle,
|
||||
IsNew = false
|
||||
};
|
||||
}
|
||||
|
||||
private async Task LoadCatalogs()
|
||||
{
|
||||
isLoading = true;
|
||||
@@ -344,11 +228,229 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
CaptureColumnLayoutFromGrid();
|
||||
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
|
||||
infoMessage = "Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ResetLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
return;
|
||||
|
||||
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
||||
|
||||
bandLayout = new BandLayout();
|
||||
columnBandAssignments.Clear();
|
||||
UpdateBandOptions();
|
||||
|
||||
foreach (var column in columnDefinitions)
|
||||
column.Width = null;
|
||||
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
_sizeMode = SizeMode.Medium;
|
||||
|
||||
if (gridRef != null)
|
||||
gridRef.LoadLayout(new GridPersistentLayout());
|
||||
gridLayoutApplied = false;
|
||||
|
||||
infoMessage = "Layout zurückgesetzt.";
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private void CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null)
|
||||
return;
|
||||
|
||||
var layout = gridRef.SaveLayout();
|
||||
bandLayout.GridLayout = layout;
|
||||
bandLayout.SizeMode = _sizeMode;
|
||||
|
||||
var orderedColumns = layout.Columns
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
||||
.OrderBy(c => c.VisibleIndex)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
||||
bandLayout.ColumnWidths = orderedColumns
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
||||
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void ApplyColumnLayoutFromStorage()
|
||||
{
|
||||
foreach (var column in columnDefinitions)
|
||||
{
|
||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||
column.Width = width;
|
||||
}
|
||||
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AddBand()
|
||||
{
|
||||
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void RemoveBand(BandDefinition band)
|
||||
{
|
||||
bandLayout.Bands.Remove(band);
|
||||
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
|
||||
columnBandAssignments.Remove(key);
|
||||
UpdateBandOptions();
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private void UpdateBandCaption(BandDefinition band, string value)
|
||||
{
|
||||
band.Caption = value;
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void UpdateColumnBand(string fieldName, string? bandId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bandId))
|
||||
columnBandAssignments.Remove(fieldName);
|
||||
else
|
||||
columnBandAssignments[fieldName] = bandId;
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private string GetColumnBand(string fieldName)
|
||||
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||
|
||||
private void SyncBandsFromAssignments()
|
||||
{
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
band.Columns = columnDefinitions
|
||||
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
|
||||
.Select(c => c.FieldName)
|
||||
.ToList();
|
||||
}
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void UpdateBandOptions()
|
||||
{
|
||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
||||
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
|
||||
}
|
||||
|
||||
private RenderFragment RenderColumns() => builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Width", "120px");
|
||||
builder.CloseComponent();
|
||||
|
||||
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
if (band.Columns.Count == 0) continue;
|
||||
|
||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||||
{
|
||||
var bandSeq = 0;
|
||||
foreach (var columnName in band.Columns)
|
||||
{
|
||||
if (columnLookup.TryGetValue(columnName, out var column))
|
||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||||
}
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
|
||||
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
||||
{
|
||||
builder.OpenComponent<DxGridDataColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||||
if (!string.IsNullOrWhiteSpace(column.Width))
|
||||
builder.AddAttribute(seq++, "Width", column.Width);
|
||||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||||
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
||||
if (column.ReadOnly)
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void SetEditContext(EditContext context)
|
||||
{
|
||||
if (editContext == context) return;
|
||||
if (editContext != null)
|
||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
||||
editContext = context;
|
||||
validationMessageStore = new ValidationMessageStore(editContext);
|
||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
||||
}
|
||||
|
||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||
{
|
||||
if (validationMessageStore == null || editContext == null) return;
|
||||
|
||||
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.UpdateProcedure))
|
||||
{
|
||||
validationMessageStore.Clear();
|
||||
editContext.NotifyValidationStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.FieldIdentifier.FieldName == nameof(CatalogEditModel.CatTitle))
|
||||
{
|
||||
validationMessageStore.Clear(new FieldIdentifier(editContext.Model, nameof(CatalogEditModel.CatTitle)));
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
|
||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
{
|
||||
popupHeaderText = e.IsNew ? "Neu" : "Edit";
|
||||
if (e.IsNew)
|
||||
{
|
||||
e.EditModel = new CatalogEditModel { IsNew = true };
|
||||
return;
|
||||
}
|
||||
|
||||
var item = (CatalogReadDto)e.DataItem;
|
||||
e.EditModel = new CatalogEditModel
|
||||
{
|
||||
Guid = item.Guid,
|
||||
CatTitle = item.CatTitle,
|
||||
CatString = item.CatString,
|
||||
UpdateProcedure = 0,
|
||||
OriginalCatTitle = item.CatTitle,
|
||||
IsNew = false
|
||||
};
|
||||
}
|
||||
|
||||
private async Task OnEditModelSaving(GridEditModelSavingEventArgs e)
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
validationMessageStore?.Clear();
|
||||
editContext?.NotifyValidationStateChanged();
|
||||
|
||||
@@ -374,18 +476,12 @@ else
|
||||
if (!created.Success || created.Value == null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(created.Error))
|
||||
{
|
||||
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), created.Error);
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMessage = "Anlegen fehlgeschlagen.";
|
||||
}
|
||||
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog angelegt.";
|
||||
focusedRowKey = created.Value.Guid;
|
||||
}
|
||||
@@ -398,7 +494,6 @@ else
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog aktualisiert.";
|
||||
focusedRowKey = editModel.Guid;
|
||||
}
|
||||
@@ -414,30 +509,20 @@ else
|
||||
|
||||
private void AddValidationError(CatalogEditModel editModel, string fieldName, string message)
|
||||
{
|
||||
if (editContext == null || validationMessageStore == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var field = new FieldIdentifier(editModel, fieldName);
|
||||
validationMessageStore.Add(field, message);
|
||||
if (editContext == null || validationMessageStore == null) return;
|
||||
validationMessageStore.Add(new FieldIdentifier(editModel, fieldName), message);
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
|
||||
private bool ValidateEditModel(CatalogEditModel editModel, bool isNew)
|
||||
{
|
||||
if (isNew)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isNew) return true;
|
||||
if (editModel.UpdateProcedure == 0 &&
|
||||
!string.Equals(editModel.CatTitle, editModel.OriginalCatTitle, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht geändert werden.");
|
||||
AddValidationError(editModel, nameof(CatalogEditModel.CatTitle), "Titel kann nicht geändert werden.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -445,346 +530,26 @@ else
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
var item = (CatalogReadDto)e.DataItem;
|
||||
|
||||
try
|
||||
{
|
||||
var deleted = await Api.DeleteAsync(item.Guid);
|
||||
if (!deleted.Success)
|
||||
{
|
||||
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
|
||||
errorMessage = deleted.Error ?? "Löschen fehlgeschlagen.";
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
|
||||
infoMessage = "Katalog gelöscht.";
|
||||
infoMessage = "Katalog gelöscht.";
|
||||
await LoadCatalogs();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Fehler beim Löschen: {ex.Message}";
|
||||
errorMessage = $"Fehler beim Löschen: {ex.Message}";
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureLayoutUserAsync()
|
||||
{
|
||||
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
layoutUser = Guid.NewGuid().ToString("N");
|
||||
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CaptureColumnLayoutFromGrid();
|
||||
|
||||
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
|
||||
await LayoutApi.UpsertAsync(new LayoutDto
|
||||
{
|
||||
LayoutType = LayoutType,
|
||||
LayoutKey = LayoutKey,
|
||||
UserName = layoutUser,
|
||||
LayoutData = layoutData
|
||||
});
|
||||
infoMessage = "Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var layout = gridRef.SaveLayout();
|
||||
bandLayout.GridLayout = layout;
|
||||
|
||||
var orderedColumns = layout.Columns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
||||
.OrderBy(column => column.VisibleIndex)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnOrder = orderedColumns
|
||||
.Select(column => column.FieldName)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnWidths = orderedColumns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task LoadBandLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
bandLayout = new BandLayout();
|
||||
UpdateBandOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
|
||||
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
|
||||
{
|
||||
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
|
||||
bandLayout = NormalizeBandLayout(parsed);
|
||||
}
|
||||
else
|
||||
{
|
||||
bandLayout = new BandLayout();
|
||||
}
|
||||
|
||||
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
|
||||
ApplyColumnLayoutFromStorage();
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private async Task ResetBandLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
||||
bandLayout = new BandLayout();
|
||||
columnBandAssignments.Clear();
|
||||
UpdateBandOptions();
|
||||
infoMessage = "Band-Layout zurückgesetzt.";
|
||||
}
|
||||
|
||||
private void ApplyColumnLayoutFromStorage()
|
||||
{
|
||||
foreach (var column in columnDefinitions)
|
||||
{
|
||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||
{
|
||||
column.Width = width;
|
||||
}
|
||||
}
|
||||
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AddBand()
|
||||
{
|
||||
bandLayout.Bands.Add(new BandDefinition
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Caption = "Band"
|
||||
});
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void RemoveBand(BandDefinition band)
|
||||
{
|
||||
bandLayout.Bands.Remove(band);
|
||||
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
|
||||
.Select(pair => pair.Key)
|
||||
.ToList();
|
||||
foreach (var column in removedColumns)
|
||||
{
|
||||
columnBandAssignments.Remove(column);
|
||||
}
|
||||
UpdateBandOptions();
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private void UpdateBandCaption(BandDefinition band, string value)
|
||||
{
|
||||
band.Caption = value;
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void UpdateColumnBand(string fieldName, string? bandId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bandId))
|
||||
{
|
||||
columnBandAssignments.Remove(fieldName);
|
||||
}
|
||||
else
|
||||
{
|
||||
columnBandAssignments[fieldName] = bandId;
|
||||
}
|
||||
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private string GetColumnBand(string fieldName)
|
||||
{
|
||||
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||
}
|
||||
|
||||
private void SyncBandsFromAssignments()
|
||||
{
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
band.Columns = columnDefinitions
|
||||
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
|
||||
.Select(column => column.FieldName)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void UpdateBandOptions()
|
||||
{
|
||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
||||
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
|
||||
}
|
||||
|
||||
private BandLayout NormalizeBandLayout(BandLayout? layout)
|
||||
{
|
||||
layout ??= new BandLayout();
|
||||
layout.Bands ??= new List<BandDefinition>();
|
||||
layout.ColumnOrder ??= new List<string>();
|
||||
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var band in layout.Bands)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(band.Id))
|
||||
{
|
||||
band.Id = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(band.Caption))
|
||||
{
|
||||
band.Caption = "Band";
|
||||
}
|
||||
|
||||
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
|
||||
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
|
||||
{
|
||||
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var band in layout.Bands)
|
||||
{
|
||||
foreach (var column in band.Columns)
|
||||
{
|
||||
assignments[column] = band.Id;
|
||||
}
|
||||
}
|
||||
|
||||
return assignments;
|
||||
}
|
||||
|
||||
private RenderFragment RenderColumns() => builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Width", "120px");
|
||||
builder.CloseComponent();
|
||||
|
||||
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName)))
|
||||
{
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
if (band.Columns.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||
builder.AddAttribute(seq++, "Columns", (RenderFragment)(bandBuilder =>
|
||||
{
|
||||
var bandSeq = 0;
|
||||
foreach (var columnName in band.Columns)
|
||||
{
|
||||
if (columnLookup.TryGetValue(columnName, out var column))
|
||||
{
|
||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||||
}
|
||||
}
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
};
|
||||
|
||||
private void BuildDataColumn(RenderTreeBuilder builder, ref int seq, ColumnDefinition column)
|
||||
{
|
||||
builder.OpenComponent<DxGridDataColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||||
if (!string.IsNullOrWhiteSpace(column.Width))
|
||||
{
|
||||
builder.AddAttribute(seq++, "Width", column.Width);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||||
{
|
||||
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
||||
}
|
||||
|
||||
if (column.ReadOnly)
|
||||
{
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
}
|
||||
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private sealed class BandLayout
|
||||
{
|
||||
public List<BandDefinition> Bands { get; set; } = new();
|
||||
public List<string> ColumnOrder { get; set; } = new();
|
||||
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public GridPersistentLayout? GridLayout { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BandDefinition
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
public List<string> Columns { get; set; } = new();
|
||||
}
|
||||
|
||||
private sealed class BandOption
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class ColumnDefinition
|
||||
{
|
||||
public string FieldName { get; init; } = string.Empty;
|
||||
public string Caption { get; init; } = string.Empty;
|
||||
public string? Width { get; set; }
|
||||
public string? DisplayFormat { get; init; }
|
||||
public bool ReadOnly { get; init; }
|
||||
public ColumnFilterType FilterType { get; init; }
|
||||
}
|
||||
|
||||
private enum ColumnFilterType
|
||||
{
|
||||
Text,
|
||||
Date
|
||||
}
|
||||
|
||||
private sealed class CatalogEditModel
|
||||
{
|
||||
public int Guid { get; set; }
|
||||
@@ -800,10 +565,4 @@ else
|
||||
public int Value { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class FilterOperatorOption
|
||||
{
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
DbFirst.BlazorWebApp/Components/CatalogsGrid.razor.css
Normal file
3
DbFirst.BlazorWebApp/Components/CatalogsGrid.razor.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.catalog-edit-popup {
|
||||
min-width: 720px;
|
||||
}
|
||||
@@ -1,68 +1,5 @@
|
||||
@using System.Text.Json
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Rendering
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.Data.Filtering
|
||||
@inject MassDataApiClient Api
|
||||
@inject LayoutApiClient LayoutApi
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
.grid-section { margin-top: 12px; }
|
||||
.pager-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.page-size-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
.page-size-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.page-size-combo {
|
||||
width: 13ch;
|
||||
min-width: 13ch;
|
||||
max-width: 13ch;
|
||||
}
|
||||
.page-size-combo input {
|
||||
text-align: left;
|
||||
}
|
||||
.massdata-edit-popup {
|
||||
min-width: 720px;
|
||||
}
|
||||
.band-editor {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.band-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.band-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
.band-columns {
|
||||
max-width: 720px;
|
||||
}
|
||||
.loading-container {
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@inject BandLayoutService BandLayoutService
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
{
|
||||
@@ -83,12 +20,12 @@ else if (!string.IsNullOrWhiteSpace(infoMessage))
|
||||
}
|
||||
else if (items.Count == 0)
|
||||
{
|
||||
<p>Keine Einträge vorhanden.</p>
|
||||
<p>Keine Einträge vorhanden.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<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>
|
||||
<DxComboBox Data="@pageSizeOptions"
|
||||
TData="PageSizeOption"
|
||||
TValue="int?"
|
||||
@@ -101,9 +38,9 @@ else
|
||||
|
||||
<div class="band-editor">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Layout speichern" Click="SaveLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||
<DxButton Text="Layout zurücksetzen" Click="ResetLayoutAsync" />
|
||||
</div>
|
||||
@foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
@@ -175,7 +112,9 @@ else
|
||||
@RenderColumns()
|
||||
</Columns>
|
||||
<EditFormTemplate Context="editFormContext">
|
||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
||||
@{
|
||||
SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew);
|
||||
}
|
||||
<DxFormLayout ColCount="2">
|
||||
<DxFormLayoutItem Caption="CustomerName">
|
||||
<DxTextBox @bind-Text="editModel.CustomerName" Width="100%" />
|
||||
@@ -233,13 +172,13 @@ else
|
||||
private int? focusedRowKey;
|
||||
private const string LayoutType = "GRID_BANDS";
|
||||
private const string LayoutKey = "MassDataGrid";
|
||||
private const string LayoutUserStorageKey = "layoutUser";
|
||||
private string? layoutUser;
|
||||
private BandLayout bandLayout = new();
|
||||
private Dictionary<string, string> columnBandAssignments = new();
|
||||
private List<BandOption> bandOptions = new();
|
||||
private Dictionary<string, ColumnDefinition> columnLookup = new();
|
||||
private readonly JsonSerializerOptions jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
private bool gridLayoutApplied;
|
||||
|
||||
private List<ColumnDefinition> columnDefinitions = new()
|
||||
{
|
||||
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
@@ -251,15 +190,13 @@ else
|
||||
new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Changed", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
||||
};
|
||||
|
||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||
|
||||
private readonly List<PageSizeOption> pageSizeOptions = new()
|
||||
{
|
||||
new() { Value = 100, Text = "100" },
|
||||
new() { Value = 1000, Text = "1.000" },
|
||||
new() { Value = 10000, Text = "10.000" },
|
||||
new() { Value = 100, Text = "100" },
|
||||
new() { Value = 1000, Text = "1.000" },
|
||||
new() { Value = 10000, Text = "10.000" },
|
||||
new() { Value = 100000, Text = "100.000" },
|
||||
new() { Value = null, Text = "Alle" }
|
||||
new() { Value = null, Text = "Alle" }
|
||||
};
|
||||
|
||||
private readonly List<ProcedureOption> procedureOptions = new()
|
||||
@@ -267,33 +204,46 @@ else
|
||||
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
|
||||
};
|
||||
|
||||
private bool gridLayoutApplied;
|
||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||
|
||||
private DevExpress.Blazor.SizeMode _sizeMode = DevExpress.Blazor.SizeMode.Medium;
|
||||
private static readonly List<DevExpress.Blazor.SizeMode> _sizeModes =
|
||||
Enum.GetValues<DevExpress.Blazor.SizeMode>().ToList();
|
||||
private SizeMode _sizeMode = SizeMode.Medium;
|
||||
private static readonly List<SizeMode> _sizeModes = Enum.GetValues<SizeMode>().ToList();
|
||||
|
||||
private string FormatSizeText(DevExpress.Blazor.SizeMode size) => size switch
|
||||
private string FormatSizeText(SizeMode size) => size switch
|
||||
{
|
||||
DevExpress.Blazor.SizeMode.Small => "Klein",
|
||||
DevExpress.Blazor.SizeMode.Medium => "Mittel",
|
||||
DevExpress.Blazor.SizeMode.Large => "Groß",
|
||||
SizeMode.Small => "Klein",
|
||||
SizeMode.Medium => "Mittel",
|
||||
SizeMode.Large => "Groß",
|
||||
_ => size.ToString()
|
||||
};
|
||||
|
||||
private void OnSizeChange(DropDownButtonItemClickEventArgs args)
|
||||
{
|
||||
_sizeMode = Enum.Parse<DevExpress.Blazor.SizeMode>(args.ItemInfo.Id);
|
||||
_sizeMode = Enum.Parse<SizeMode>(args.ItemInfo.Id);
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
await EnsureLayoutUserAsync();
|
||||
await LoadBandLayoutAsync();
|
||||
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
layoutUser = await BandLayoutService.EnsureLayoutUserAsync();
|
||||
bandLayout = await BandLayoutService.LoadBandLayoutAsync(LayoutType, LayoutKey, layoutUser, columnLookup);
|
||||
columnBandAssignments = BandLayoutService.BuildAssignmentsFromLayout(bandLayout);
|
||||
ApplyColumnLayoutFromStorage();
|
||||
_sizeMode = bandLayout.SizeMode;
|
||||
UpdateBandOptions();
|
||||
await LoadPage(0);
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||
{
|
||||
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||
gridLayoutApplied = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadPage(int page)
|
||||
{
|
||||
isLoading = true;
|
||||
@@ -304,7 +254,6 @@ else
|
||||
var effectivePageSize = pageSize ?? (total == 0 ? 1 : total);
|
||||
pageCount = Math.Max(1, (int)Math.Ceiling(total / (double)effectivePageSize));
|
||||
pageIndex = Math.Clamp(page, 0, pageCount - 1);
|
||||
|
||||
var skip = pageSize.HasValue ? pageIndex * pageSize.Value : 0;
|
||||
items = await Api.GetAllAsync(skip, pageSize);
|
||||
}
|
||||
@@ -320,10 +269,7 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OnPageChanged(int index)
|
||||
{
|
||||
await LoadPage(index);
|
||||
}
|
||||
private async Task OnPageChanged(int index) => await LoadPage(index);
|
||||
|
||||
private async Task OnPageSizeChanged(int? size)
|
||||
{
|
||||
@@ -331,60 +277,15 @@ else
|
||||
await LoadPage(0);
|
||||
}
|
||||
|
||||
private async Task EnsureLayoutUserAsync()
|
||||
{
|
||||
layoutUser = await JsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
layoutUser = Guid.NewGuid().ToString("N");
|
||||
await JsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadBandLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
bandLayout = new BandLayout();
|
||||
UpdateBandOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
var stored = await LayoutApi.GetAsync(LayoutType, LayoutKey, layoutUser);
|
||||
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
|
||||
{
|
||||
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, jsonOptions);
|
||||
bandLayout = NormalizeBandLayout(parsed);
|
||||
}
|
||||
else
|
||||
{
|
||||
bandLayout = new BandLayout();
|
||||
}
|
||||
|
||||
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
|
||||
ApplyColumnLayoutFromStorage();
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private async Task SaveLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
CaptureColumnLayoutFromGrid();
|
||||
|
||||
var layoutData = JsonSerializer.Serialize(bandLayout, jsonOptions);
|
||||
await LayoutApi.UpsertAsync(new LayoutDto
|
||||
{
|
||||
LayoutType = LayoutType,
|
||||
LayoutKey = LayoutKey,
|
||||
UserName = layoutUser,
|
||||
LayoutData = layoutData
|
||||
});
|
||||
await BandLayoutService.SaveBandLayoutAsync(LayoutType, LayoutKey, layoutUser, bandLayout);
|
||||
infoMessage = "Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
@@ -394,42 +295,49 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var layout = gridRef.SaveLayout();
|
||||
bandLayout.GridLayout = layout;
|
||||
|
||||
var orderedColumns = layout.Columns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
||||
.OrderBy(column => column.VisibleIndex)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnOrder = orderedColumns
|
||||
.Select(column => column.FieldName)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnWidths = orderedColumns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private async Task ResetBandLayoutAsync()
|
||||
private async Task ResetLayoutAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
|
||||
await BandLayoutService.ResetBandLayoutAsync(LayoutType, LayoutKey, layoutUser);
|
||||
|
||||
bandLayout = new BandLayout();
|
||||
columnBandAssignments.Clear();
|
||||
UpdateBandOptions();
|
||||
infoMessage = "Band-Layout zurückgesetzt.";
|
||||
|
||||
foreach (var column in columnDefinitions)
|
||||
column.Width = null;
|
||||
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
_sizeMode = SizeMode.Medium;
|
||||
|
||||
if (gridRef != null)
|
||||
gridRef.LoadLayout(new GridPersistentLayout());
|
||||
gridLayoutApplied = false;
|
||||
|
||||
infoMessage = "Layout zurückgesetzt.";
|
||||
errorMessage = null;
|
||||
}
|
||||
|
||||
private void CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null)
|
||||
return;
|
||||
|
||||
var layout = gridRef.SaveLayout();
|
||||
bandLayout.GridLayout = layout;
|
||||
bandLayout.SizeMode = _sizeMode;
|
||||
|
||||
var orderedColumns = layout.Columns
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.FieldName))
|
||||
.OrderBy(c => c.VisibleIndex)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnOrder = orderedColumns.Select(c => c.FieldName).ToList();
|
||||
bandLayout.ColumnWidths = orderedColumns
|
||||
.Where(c => !string.IsNullOrWhiteSpace(c.Width))
|
||||
.ToDictionary(c => c.FieldName, c => c.Width, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void ApplyColumnLayoutFromStorage()
|
||||
@@ -437,34 +345,22 @@ else
|
||||
foreach (var column in columnDefinitions)
|
||||
{
|
||||
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
|
||||
{
|
||||
column.Width = width;
|
||||
}
|
||||
}
|
||||
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
columnLookup = columnDefinitions.ToDictionary(c => c.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private void AddBand()
|
||||
{
|
||||
bandLayout.Bands.Add(new BandDefinition
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Caption = "Band"
|
||||
});
|
||||
bandLayout.Bands.Add(new BandDefinition { Id = Guid.NewGuid().ToString("N"), Caption = "Band" });
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void RemoveBand(BandDefinition band)
|
||||
{
|
||||
bandLayout.Bands.Remove(band);
|
||||
var removedColumns = columnBandAssignments.Where(pair => pair.Value == band.Id)
|
||||
.Select(pair => pair.Key)
|
||||
.ToList();
|
||||
foreach (var column in removedColumns)
|
||||
{
|
||||
columnBandAssignments.Remove(column);
|
||||
}
|
||||
foreach (var key in columnBandAssignments.Where(p => p.Value == band.Id).Select(p => p.Key).ToList())
|
||||
columnBandAssignments.Remove(key);
|
||||
UpdateBandOptions();
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
@@ -478,77 +374,31 @@ else
|
||||
private void UpdateColumnBand(string fieldName, string? bandId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(bandId))
|
||||
{
|
||||
columnBandAssignments.Remove(fieldName);
|
||||
}
|
||||
else
|
||||
{
|
||||
columnBandAssignments[fieldName] = bandId;
|
||||
}
|
||||
|
||||
SyncBandsFromAssignments();
|
||||
}
|
||||
|
||||
private string GetColumnBand(string fieldName)
|
||||
{
|
||||
return columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||
}
|
||||
=> columnBandAssignments.TryGetValue(fieldName, out var bandId) ? bandId : string.Empty;
|
||||
|
||||
private void SyncBandsFromAssignments()
|
||||
{
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
band.Columns = columnDefinitions
|
||||
.Where(column => columnBandAssignments.TryGetValue(column.FieldName, out var bandId) && bandId == band.Id)
|
||||
.Select(column => column.FieldName)
|
||||
.Where(c => columnBandAssignments.TryGetValue(c.FieldName, out var id) && id == band.Id)
|
||||
.Select(c => c.FieldName)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private void UpdateBandOptions()
|
||||
{
|
||||
bandOptions = new List<BandOption> { new() { Id = string.Empty, Caption = "Ohne Band" } };
|
||||
bandOptions.AddRange(bandLayout.Bands.Select(band => new BandOption { Id = band.Id, Caption = band.Caption }));
|
||||
}
|
||||
|
||||
private Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
|
||||
{
|
||||
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var band in layout.Bands)
|
||||
{
|
||||
foreach (var column in band.Columns)
|
||||
{
|
||||
assignments[column] = band.Id;
|
||||
}
|
||||
}
|
||||
|
||||
return assignments;
|
||||
}
|
||||
|
||||
private BandLayout NormalizeBandLayout(BandLayout? layout)
|
||||
{
|
||||
layout ??= new BandLayout();
|
||||
layout.Bands ??= new List<BandDefinition>();
|
||||
layout.ColumnOrder ??= new List<string>();
|
||||
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var band in layout.Bands)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(band.Id))
|
||||
{
|
||||
band.Id = Guid.NewGuid().ToString("N");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(band.Caption))
|
||||
{
|
||||
band.Caption = "Band";
|
||||
}
|
||||
|
||||
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
return layout;
|
||||
bandOptions.AddRange(bandLayout.Bands.Select(b => new BandOption { Id = b.Id, Caption = b.Caption }));
|
||||
}
|
||||
|
||||
private RenderFragment RenderColumns() => builder =>
|
||||
@@ -558,18 +408,13 @@ else
|
||||
builder.AddAttribute(seq++, "Width", "120px");
|
||||
builder.CloseComponent();
|
||||
|
||||
var grouped = bandLayout.Bands.SelectMany(band => band.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var column in columnDefinitions.Where(column => !grouped.Contains(column.FieldName)))
|
||||
{
|
||||
var grouped = bandLayout.Bands.SelectMany(b => b.Columns).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var column in columnDefinitions.Where(c => !grouped.Contains(c.FieldName)))
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
if (band.Columns.Count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (band.Columns.Count == 0) continue;
|
||||
|
||||
builder.OpenComponent<DxGridBandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Caption", band.Caption);
|
||||
@@ -579,9 +424,7 @@ else
|
||||
foreach (var columnName in band.Columns)
|
||||
{
|
||||
if (columnLookup.TryGetValue(columnName, out var column))
|
||||
{
|
||||
BuildDataColumn(bandBuilder, ref bandSeq, column);
|
||||
}
|
||||
}
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
@@ -594,35 +437,19 @@ else
|
||||
builder.AddAttribute(seq++, "FieldName", column.FieldName);
|
||||
builder.AddAttribute(seq++, "Caption", column.Caption);
|
||||
if (!string.IsNullOrWhiteSpace(column.Width))
|
||||
{
|
||||
builder.AddAttribute(seq++, "Width", column.Width);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(column.DisplayFormat))
|
||||
{
|
||||
builder.AddAttribute(seq++, "DisplayFormat", column.DisplayFormat);
|
||||
}
|
||||
|
||||
if (column.ReadOnly)
|
||||
{
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
}
|
||||
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void SetEditContext(EditContext context)
|
||||
{
|
||||
if (editContext == context)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (editContext == context) return;
|
||||
if (editContext != null)
|
||||
{
|
||||
editContext.OnFieldChanged -= OnEditFieldChanged;
|
||||
}
|
||||
|
||||
editContext = context;
|
||||
validationMessageStore = new ValidationMessageStore(editContext);
|
||||
editContext.OnFieldChanged += OnEditFieldChanged;
|
||||
@@ -630,10 +457,7 @@ else
|
||||
|
||||
private void OnEditFieldChanged(object? sender, FieldChangedEventArgs e)
|
||||
{
|
||||
if (validationMessageStore == null || editContext == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
if (validationMessageStore == null || editContext == null) return;
|
||||
|
||||
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.UpdateProcedure))
|
||||
{
|
||||
@@ -644,16 +468,12 @@ else
|
||||
|
||||
if (e.FieldIdentifier.FieldName == nameof(MassDataEditModel.CustomerName))
|
||||
{
|
||||
var field = new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName));
|
||||
validationMessageStore.Clear(field);
|
||||
validationMessageStore.Clear(new FieldIdentifier(editContext.Model, nameof(MassDataEditModel.CustomerName)));
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPopupHeaderText(bool isNew)
|
||||
{
|
||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
}
|
||||
private void SetPopupHeaderText(bool isNew) => popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
|
||||
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
{
|
||||
@@ -683,14 +503,13 @@ else
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = null;
|
||||
|
||||
validationMessageStore?.Clear();
|
||||
editContext?.NotifyValidationStateChanged();
|
||||
|
||||
var editModel = (MassDataEditModel)e.EditModel;
|
||||
if (!decimal.TryParse(editModel.AmountText, out var amount))
|
||||
{
|
||||
AddValidationError(editModel, nameof(MassDataEditModel.AmountText), "Amount ist ungültig.");
|
||||
AddValidationError(editModel, nameof(MassDataEditModel.AmountText), "Amount ist ungültig.");
|
||||
e.Cancel = true;
|
||||
return;
|
||||
}
|
||||
@@ -730,72 +549,19 @@ else
|
||||
|
||||
private void AddValidationError(MassDataEditModel editModel, string fieldName, string message)
|
||||
{
|
||||
if (editContext == null || validationMessageStore == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var field = new FieldIdentifier(editModel, fieldName);
|
||||
validationMessageStore.Add(field, message);
|
||||
if (editContext == null || validationMessageStore == null) return;
|
||||
validationMessageStore.Add(new FieldIdentifier(editModel, fieldName), message);
|
||||
editContext.NotifyValidationStateChanged();
|
||||
}
|
||||
|
||||
private Task OnDataItemDeleting(GridDataItemDeletingEventArgs e)
|
||||
{
|
||||
errorMessage = null;
|
||||
infoMessage = "Löschen ist aktuell noch nicht verfügbar.";
|
||||
infoMessage = "Löschen ist aktuell noch nicht verfügbar.";
|
||||
e.Cancel = true;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
|
||||
{
|
||||
gridRef.LoadLayout(bandLayout.GridLayout);
|
||||
gridLayoutApplied = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BandLayout
|
||||
{
|
||||
public List<BandDefinition> Bands { get; set; } = new();
|
||||
public List<string> ColumnOrder { get; set; } = new();
|
||||
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public GridPersistentLayout? GridLayout { get; set; }
|
||||
}
|
||||
|
||||
private sealed class BandDefinition
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
public List<string> Columns { get; set; } = new();
|
||||
}
|
||||
|
||||
private sealed class BandOption
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class ColumnDefinition
|
||||
{
|
||||
public string FieldName { get; init; } = string.Empty;
|
||||
public string Caption { get; init; } = string.Empty;
|
||||
public string? Width { get; set; }
|
||||
public string? DisplayFormat { get; init; }
|
||||
public bool ReadOnly { get; init; }
|
||||
public ColumnFilterType FilterType { get; init; }
|
||||
}
|
||||
|
||||
private enum ColumnFilterType
|
||||
{
|
||||
Text,
|
||||
Bool,
|
||||
Date
|
||||
}
|
||||
|
||||
private sealed class MassDataEditModel
|
||||
{
|
||||
public int Id { get; set; }
|
||||
@@ -814,21 +580,9 @@ else
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class BoolFilterOption
|
||||
{
|
||||
public bool? Value { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class PageSizeOption
|
||||
{
|
||||
public int? Value { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
private sealed class FilterOperatorOption
|
||||
{
|
||||
public string Value { get; set; } = string.Empty;
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
3
DbFirst.BlazorWebApp/Components/MassDataGrid.razor.css
Normal file
3
DbFirst.BlazorWebApp/Components/MassDataGrid.razor.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.massdata-edit-popup {
|
||||
min-width: 720px;
|
||||
}
|
||||
@@ -5,46 +5,6 @@
|
||||
@inject NavigationManager Navigation
|
||||
@inject DashboardApiClient DashboardApi
|
||||
|
||||
<style>
|
||||
.dashboard-shell {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
min-height: 800px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
.dashboard-nav {
|
||||
width: 220px;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
background: #fafafa;
|
||||
}
|
||||
.dashboard-nav-title {
|
||||
padding: 0.75rem 1rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dashboard-nav-link {
|
||||
display: block;
|
||||
padding: 0.55rem 1rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.dashboard-nav-link.active {
|
||||
background: #e9ecef;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dashboard-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<PageTitle>Dashboards</PageTitle>
|
||||
|
||||
<div class="dashboard-shell">
|
||||
|
||||
42
DbFirst.BlazorWebApp/Components/Pages/Dashboard.razor.css
Normal file
42
DbFirst.BlazorWebApp/Components/Pages/Dashboard.razor.css
Normal file
@@ -0,0 +1,42 @@
|
||||
.dashboard-shell {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
min-height: 800px;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.dashboard-nav {
|
||||
width: 220px;
|
||||
border-right: 1px solid #e6e6e6;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.dashboard-nav-title {
|
||||
padding: 0.75rem 1rem 0.5rem;
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-nav-link {
|
||||
display: block;
|
||||
padding: 0.55rem 1rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dashboard-nav-link.active {
|
||||
background: #e9ecef;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.dashboard-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
@using System.Net.Http
|
||||
@using System.Net.Http.Json
|
||||
@using System.Text.Json
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.AspNetCore.Components.Rendering
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@@ -10,8 +12,9 @@
|
||||
@using DbFirst.BlazorWebApp
|
||||
@using DbFirst.BlazorWebApp.Components
|
||||
@using DbFirst.BlazorWebApp.Models
|
||||
@using DbFirst.BlazorWebApp.Models.Grid
|
||||
@using DbFirst.BlazorWebApp.Services
|
||||
@using DevExpress.Blazor
|
||||
@using DevExpress.DashboardBlazor
|
||||
@using DevExpress.DashboardWeb
|
||||
@using DbFirst.BlazorWebApp
|
||||
@using DevExpress.Data.Filtering
|
||||
@@ -11,9 +11,6 @@
|
||||
<PackageReference Include="DevExpress.Blazor.Dashboard" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Themes" Version="25.2.3" />
|
||||
<PackageReference Include="DevExpress.Blazor.Themes.Fluent" Version="25.2.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DevExpress.Blazor" Version="25.2.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.22" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
43
DbFirst.BlazorWebApp/Models/Grid/BandLayoutModels.cs
Normal file
43
DbFirst.BlazorWebApp/Models/Grid/BandLayoutModels.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using DevExpress.Blazor;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Models.Grid
|
||||
{
|
||||
public class BandLayout
|
||||
{
|
||||
public List<BandDefinition> Bands { get; set; } = new();
|
||||
public List<string> ColumnOrder { get; set; } = new();
|
||||
public Dictionary<string, string?> ColumnWidths { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public GridPersistentLayout? GridLayout { get; set; }
|
||||
public SizeMode SizeMode { get; set; } = SizeMode.Medium;
|
||||
}
|
||||
|
||||
public class BandDefinition
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
public List<string> Columns { get; set; } = new();
|
||||
}
|
||||
|
||||
public class BandOption
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Caption { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ColumnDefinition
|
||||
{
|
||||
public string FieldName { get; init; } = string.Empty;
|
||||
public string Caption { get; init; } = string.Empty;
|
||||
public string? Width { get; set; }
|
||||
public string? DisplayFormat { get; init; }
|
||||
public bool ReadOnly { get; init; }
|
||||
public ColumnFilterType FilterType { get; init; }
|
||||
}
|
||||
|
||||
public enum ColumnFilterType
|
||||
{
|
||||
Text,
|
||||
Bool,
|
||||
Date
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ builder.Services.AddRazorComponents()
|
||||
|
||||
builder.Services.AddDevExpressBlazor(options => options.BootstrapVersion = BootstrapVersion.v5);
|
||||
builder.Services.AddScoped<ThemeState>();
|
||||
builder.Services.AddScoped<BandLayoutService>();
|
||||
|
||||
var apiBaseUrl = builder.Configuration["ApiBaseUrl"];
|
||||
if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
||||
|
||||
103
DbFirst.BlazorWebApp/Services/BandLayoutService.cs
Normal file
103
DbFirst.BlazorWebApp/Services/BandLayoutService.cs
Normal file
@@ -0,0 +1,103 @@
|
||||
using DbFirst.BlazorWebApp.Models;
|
||||
using DbFirst.BlazorWebApp.Models.Grid;
|
||||
using Microsoft.JSInterop;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services
|
||||
{
|
||||
public class BandLayoutService(LayoutApiClient layoutApi, IJSRuntime jsRuntime)
|
||||
{
|
||||
private const string LayoutUserStorageKey = "layoutUser";
|
||||
private readonly JsonSerializerOptions _jsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
public async Task<string> EnsureLayoutUserAsync()
|
||||
{
|
||||
var layoutUser = await jsRuntime.InvokeAsync<string?>("localStorage.getItem", LayoutUserStorageKey);
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
{
|
||||
layoutUser = Guid.NewGuid().ToString("N");
|
||||
await jsRuntime.InvokeVoidAsync("localStorage.setItem", LayoutUserStorageKey, layoutUser);
|
||||
}
|
||||
return layoutUser;
|
||||
}
|
||||
|
||||
public async Task<BandLayout> LoadBandLayoutAsync(
|
||||
string layoutType,
|
||||
string layoutKey,
|
||||
string layoutUser,
|
||||
Dictionary<string, ColumnDefinition> columnLookup)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutUser))
|
||||
return new BandLayout();
|
||||
|
||||
var stored = await layoutApi.GetAsync(layoutType, layoutKey, layoutUser);
|
||||
if (stored != null && !string.IsNullOrWhiteSpace(stored.LayoutData))
|
||||
{
|
||||
var parsed = JsonSerializer.Deserialize<BandLayout>(stored.LayoutData, _jsonOptions);
|
||||
return NormalizeBandLayout(parsed, columnLookup);
|
||||
}
|
||||
|
||||
return new BandLayout();
|
||||
}
|
||||
|
||||
public async Task SaveBandLayoutAsync(
|
||||
string layoutType,
|
||||
string layoutKey,
|
||||
string layoutUser,
|
||||
BandLayout bandLayout)
|
||||
{
|
||||
var layoutData = JsonSerializer.Serialize(bandLayout, _jsonOptions);
|
||||
await layoutApi.UpsertAsync(new LayoutDto
|
||||
{
|
||||
LayoutType = layoutType,
|
||||
LayoutKey = layoutKey,
|
||||
UserName = layoutUser,
|
||||
LayoutData = layoutData
|
||||
});
|
||||
}
|
||||
|
||||
public async Task ResetBandLayoutAsync(
|
||||
string layoutType,
|
||||
string layoutKey,
|
||||
string layoutUser)
|
||||
{
|
||||
await layoutApi.DeleteAsync(layoutType, layoutKey, layoutUser);
|
||||
}
|
||||
|
||||
public Dictionary<string, string> BuildAssignmentsFromLayout(BandLayout layout)
|
||||
{
|
||||
var assignments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var band in layout.Bands)
|
||||
{
|
||||
foreach (var column in band.Columns)
|
||||
{
|
||||
assignments[column] = band.Id;
|
||||
}
|
||||
}
|
||||
return assignments;
|
||||
}
|
||||
|
||||
public BandLayout NormalizeBandLayout(
|
||||
BandLayout? layout,
|
||||
Dictionary<string, ColumnDefinition> columnLookup)
|
||||
{
|
||||
layout ??= new BandLayout();
|
||||
layout.Bands ??= new List<BandDefinition>();
|
||||
layout.ColumnOrder ??= new List<string>();
|
||||
layout.ColumnWidths ??= new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var band in layout.Bands)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(band.Id))
|
||||
band.Id = Guid.NewGuid().ToString("N");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(band.Caption))
|
||||
band.Caption = "Band";
|
||||
|
||||
band.Columns = band.Columns?.Where(columnLookup.ContainsKey).ToList() ?? new List<string>();
|
||||
}
|
||||
|
||||
return layout;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,7 @@ a, .btn-link {
|
||||
}
|
||||
|
||||
.btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
|
||||
}
|
||||
|
||||
.content {
|
||||
@@ -51,7 +51,7 @@ h1:focus {
|
||||
}
|
||||
|
||||
.blazor-error-boundary {
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA9NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDg2IDY2LjAxODMgMjYzLjU4NiA2Ni4wMTgzWk0yNjMuNTc2IDg2LjA1NDdDMjYxLjA0OSA4Ni4wNTQ3IDI1OS43ODUgODcuMzAwNSAxNTEuMDIyIDg5Ljc5MjEgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDg2IDY2LjAxODMgMjYzLjU4NiA2Ni4wMTgzWk0yNjMuNTc2IDg2LjA1NDdDMjYxLjA0OSA4Ni4wNTQ3IDI1OS43ODUgODcuMzAwNSAyNTkuNzg2IDg5Ljc5MjEgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
|
||||
padding: 1rem 1rem 1rem 3.7rem;
|
||||
color: white;
|
||||
}
|
||||
@@ -63,3 +63,76 @@ h1:focus {
|
||||
.darker-border-checkbox.form-check-input {
|
||||
border-color: #929292;
|
||||
}
|
||||
|
||||
/* Grid Band-Editor */
|
||||
.band-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.band-controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.band-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.band-columns {
|
||||
max-width: 720px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.grid-section {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* MassData-spezifisch */
|
||||
.page-size-selector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.page-size-label {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.page-size-combo {
|
||||
width: 13ch;
|
||||
min-width: 13ch;
|
||||
max-width: 13ch;
|
||||
}
|
||||
|
||||
.page-size-combo input {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.pager-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* Lade-Spinner */
|
||||
.loading-container {
|
||||
min-height: 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user