Add user-specific persistent grid/band layouts support
Implemented user-customizable, persistent grid and band layouts for CatalogsGrid and MassDataGrid. Added backend API, database entity, and repository for storing layouts per user. Refactored grids to support dynamic band/column rendering, layout management UI, and per-user storage via localStorage and the new API. Registered all necessary services and updated data context. Enables flexible, user-specific grid experiences with saved layouts.
This commit is contained in:
94
DbFirst.API/Controllers/LayoutsController.cs
Normal file
94
DbFirst.API/Controllers/LayoutsController.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System.Text;
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain.Entities;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace DbFirst.API.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class LayoutsController : ControllerBase
|
||||
{
|
||||
private readonly ILayoutRepository _repository;
|
||||
|
||||
public LayoutsController(ILayoutRepository repository)
|
||||
{
|
||||
_repository = repository;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult<LayoutDto>> Get([FromQuery] string layoutType, [FromQuery] string layoutKey, [FromQuery] string userName, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutType) || string.IsNullOrWhiteSpace(layoutKey) || string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
return BadRequest("layoutType, layoutKey und userName sind erforderlich.");
|
||||
}
|
||||
|
||||
var entity = await _repository.GetAsync(layoutType, layoutKey, userName, cancellationToken);
|
||||
if (entity == null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(Map(entity));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public async Task<ActionResult<LayoutDto>> Upsert(LayoutDto dto, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dto.LayoutType) || string.IsNullOrWhiteSpace(dto.LayoutKey) || string.IsNullOrWhiteSpace(dto.UserName))
|
||||
{
|
||||
return BadRequest("LayoutType, LayoutKey und UserName sind erforderlich.");
|
||||
}
|
||||
|
||||
var data = string.IsNullOrWhiteSpace(dto.LayoutData)
|
||||
? Array.Empty<byte>()
|
||||
: Encoding.UTF8.GetBytes(dto.LayoutData);
|
||||
|
||||
try
|
||||
{
|
||||
var entity = await _repository.UpsertAsync(dto.LayoutType, dto.LayoutKey, dto.UserName, data, cancellationToken);
|
||||
return Ok(Map(entity));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var detail = ex.InnerException?.Message ?? ex.Message;
|
||||
return Problem(detail: detail, statusCode: StatusCodes.Status500InternalServerError);
|
||||
}
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
public async Task<IActionResult> Delete([FromQuery] string layoutType, [FromQuery] string layoutKey, [FromQuery] string userName, CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(layoutType) || string.IsNullOrWhiteSpace(layoutKey) || string.IsNullOrWhiteSpace(userName))
|
||||
{
|
||||
return BadRequest("layoutType, layoutKey und userName sind erforderlich.");
|
||||
}
|
||||
|
||||
var deleted = await _repository.DeleteAsync(layoutType, layoutKey, userName, cancellationToken);
|
||||
return deleted ? NoContent() : NotFound();
|
||||
}
|
||||
|
||||
private static LayoutDto Map(SmfLayout entity)
|
||||
{
|
||||
var layoutData = entity.LayoutData.Length == 0
|
||||
? string.Empty
|
||||
: Encoding.UTF8.GetString(entity.LayoutData);
|
||||
|
||||
return new LayoutDto
|
||||
{
|
||||
LayoutType = entity.LayoutType,
|
||||
LayoutKey = entity.LayoutKey,
|
||||
UserName = entity.UserName,
|
||||
LayoutData = layoutData
|
||||
};
|
||||
}
|
||||
|
||||
public sealed class LayoutDto
|
||||
{
|
||||
public string LayoutType { get; set; } = string.Empty;
|
||||
public string LayoutKey { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string LayoutData { get; set; } = string.Empty;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@ builder.Services.AddApplication();
|
||||
|
||||
builder.Services.AddScoped<ICatalogRepository, CatalogRepository>();
|
||||
builder.Services.AddScoped<IMassDataRepository, MassDataRepository>();
|
||||
builder.Services.AddScoped<ILayoutRepository, LayoutRepository>();
|
||||
|
||||
builder.Services.AddDevExpressControls();
|
||||
builder.Services.AddSignalR();
|
||||
|
||||
10
DbFirst.Application/Repositories/ILayoutRepository.cs
Normal file
10
DbFirst.Application/Repositories/ILayoutRepository.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using DbFirst.Domain.Entities;
|
||||
|
||||
namespace DbFirst.Application.Repositories;
|
||||
|
||||
public interface ILayoutRepository
|
||||
{
|
||||
Task<SmfLayout?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default);
|
||||
Task<SmfLayout> UpsertAsync(string layoutType, string layoutKey, string userName, byte[] layoutData, CancellationToken cancellationToken = default);
|
||||
Task<bool> DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
@using System.Text.Json
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Rendering
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@inject CatalogApiClient Api
|
||||
@inject LayoutApiClient LayoutApi
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
@@ -48,6 +53,25 @@
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
@@ -69,11 +93,45 @@ else if (items.Count == 0)
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="band-editor">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Band-Layout speichern" Click="SaveBandLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Grid-Layout speichern" Click="SaveGridLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||
</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 class="grid-section">
|
||||
<DxGrid Data="@items"
|
||||
TItem="CatalogReadDto"
|
||||
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
||||
ShowFilterRow="true"
|
||||
AllowColumnResize="true"
|
||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||
AllowColumnReorder="true"
|
||||
PageSize="10"
|
||||
CssClass="mb-4 catalog-grid"
|
||||
EditMode="GridEditMode.PopupEditForm"
|
||||
@@ -81,58 +139,10 @@ else
|
||||
PopupEditFormHeaderText="@popupHeaderText"
|
||||
CustomizeEditModel="OnCustomizeEditModel"
|
||||
EditModelSaving="OnEditModelSaving"
|
||||
DataItemDeleting="OnDataItemDeleting">
|
||||
DataItemDeleting="OnDataItemDeleting"
|
||||
@ref="gridRef">
|
||||
<Columns>
|
||||
<DxGridCommandColumn Width="120px" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
@RenderColumns()
|
||||
</Columns>
|
||||
<EditFormTemplate Context="editFormContext">
|
||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
||||
@@ -171,7 +181,27 @@ else
|
||||
private string? infoMessage;
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
private IGrid? gridRef;
|
||||
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 List<ColumnDefinition> columnDefinitions = new()
|
||||
{
|
||||
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(CatalogReadDto.CatTitle), Caption = "Titel", FilterType = ColumnFilterType.Text },
|
||||
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 }
|
||||
};
|
||||
|
||||
private readonly List<ProcedureOption> procedureOptions = new()
|
||||
{
|
||||
@@ -179,8 +209,13 @@ else
|
||||
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
||||
};
|
||||
|
||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
await EnsureLayoutUserAsync();
|
||||
await LoadBandLayoutAsync();
|
||||
await LoadCatalogs();
|
||||
}
|
||||
|
||||
@@ -223,6 +258,11 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPopupHeaderText(bool isNew)
|
||||
{
|
||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
}
|
||||
|
||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
{
|
||||
popupHeaderText = e.IsNew ? "Neu" : "Edit";
|
||||
@@ -385,9 +425,466 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private void SetPopupHeaderText(bool isNew)
|
||||
private async Task EnsureLayoutUserAsync()
|
||||
{
|
||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
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();
|
||||
ApplyBandOrderingFromColumnOrder();
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private async Task SaveBandLayoutAsync()
|
||||
{
|
||||
await SaveGridLayoutAsync();
|
||||
}
|
||||
|
||||
private async Task SaveGridLayoutAsync()
|
||||
{
|
||||
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 = "Grid-Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Grid-Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (bandLayout.ColumnOrder.Count > 0)
|
||||
{
|
||||
var ordered = bandLayout.ColumnOrder
|
||||
.Where(columnLookup.ContainsKey)
|
||||
.Select(field => columnLookup[field])
|
||||
.ToList();
|
||||
|
||||
ordered.AddRange(columnDefinitions.Where(column => !ordered.Contains(column)));
|
||||
columnDefinitions = ordered;
|
||||
}
|
||||
|
||||
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 CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var gridColumns = gridRef.GetColumns()
|
||||
.OfType<IGridDataColumn>()
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnOrder = gridColumns
|
||||
.OrderBy(column => column.VisibleIndex)
|
||||
.Select(column => column.FieldName)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnWidths = gridColumns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
ApplyBandOrderingFromColumnOrder();
|
||||
}
|
||||
|
||||
private void ApplyBandOrderingFromColumnOrder()
|
||||
{
|
||||
if (bandLayout.ColumnOrder.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bandById = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
||||
var orderedBandIds = new List<string>();
|
||||
var orderedColumnsByBand = bandLayout.Bands.ToDictionary(
|
||||
band => band.Id,
|
||||
_ => new List<string>(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var field in bandLayout.ColumnOrder)
|
||||
{
|
||||
if (columnBandAssignments.TryGetValue(field, out var bandId) && bandById.ContainsKey(bandId))
|
||||
{
|
||||
if (!orderedBandIds.Contains(bandId, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
orderedBandIds.Add(bandId);
|
||||
}
|
||||
|
||||
orderedColumnsByBand[bandId].Add(field);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
var orderedColumns = orderedColumnsByBand[band.Id];
|
||||
orderedColumns.AddRange(band.Columns.Where(column => !orderedColumns.Contains(column, StringComparer.OrdinalIgnoreCase)));
|
||||
band.Columns = orderedColumns;
|
||||
}
|
||||
|
||||
if (orderedBandIds.Count > 0)
|
||||
{
|
||||
bandLayout.Bands = orderedBandIds
|
||||
.Select(id => bandById[id])
|
||||
.Concat(bandLayout.Bands.Where(band => !orderedBandIds.Contains(band.Id, StringComparer.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
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>();
|
||||
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 bandLookup = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
||||
var renderedBands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var orderedFields = bandLayout.ColumnOrder
|
||||
.Where(columnLookup.ContainsKey)
|
||||
.ToList();
|
||||
|
||||
if (orderedFields.Count == 0)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var fieldName in orderedFields)
|
||||
{
|
||||
if (columnBandAssignments.TryGetValue(fieldName, out var bandId) && bandLookup.TryGetValue(bandId, out var band))
|
||||
{
|
||||
if (!renderedBands.Add(bandId) || 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();
|
||||
}
|
||||
else if (columnLookup.TryGetValue(fieldName, out var column))
|
||||
{
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var column in columnDefinitions)
|
||||
{
|
||||
if (!orderedFields.Contains(column.FieldName, StringComparer.OrdinalIgnoreCase) &&
|
||||
(!columnBandAssignments.TryGetValue(column.FieldName, out var bandId) || !bandLookup.ContainsKey(bandId)))
|
||||
{
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
if (renderedBands.Contains(band.Id) || 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 RenderFragment? BuildFilterTemplate(ColumnDefinition column)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private RenderFragment? BuildTextFilterTemplate()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
private RenderFragment? BuildDateFilterTemplate()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
@using System.Text.Json
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Rendering
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@inject MassDataApiClient Api
|
||||
@inject LayoutApiClient LayoutApi
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
@@ -71,6 +76,25 @@
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
@@ -104,13 +128,45 @@ else
|
||||
CssClass="page-size-combo" />
|
||||
</div>
|
||||
|
||||
<div class="band-editor">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Band-Layout speichern" Click="SaveBandLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Grid-Layout speichern" Click="SaveGridLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||
</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 class="grid-section">
|
||||
<DxGrid Data="@items"
|
||||
TItem="MassDataReadDto"
|
||||
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
||||
ShowFilterRow="true"
|
||||
ShowGroupPanel="true"
|
||||
AllowColumnResize="true"
|
||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||
AllowColumnReorder="true"
|
||||
PagerVisible="false"
|
||||
PageSize="@(pageSize ?? 100)"
|
||||
CssClass="mb-3 massdata-grid"
|
||||
@@ -118,63 +174,10 @@ else
|
||||
PopupEditFormHeaderText="@popupHeaderText"
|
||||
CustomizeEditModel="OnCustomizeEditModel"
|
||||
EditModelSaving="OnEditModelSaving"
|
||||
DataItemDeleting="OnDataItemDeleting">
|
||||
DataItemDeleting="OnDataItemDeleting"
|
||||
@ref="gridRef">
|
||||
<Columns>
|
||||
<DxGridCommandColumn Width="120px" />
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.CustomerName)" Caption="CustomerName">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Amount)" Caption="Amount" DisplayFormat="c2">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Category)" Caption="Category" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.StatusFlag)" Caption="Status" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxComboBox Data="@statusFilterOptions"
|
||||
TData="BoolFilterOption"
|
||||
TValue="bool?"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
Value="@(filter.FilterRowValue as bool?)"
|
||||
ValueChanged="@(value => filter.FilterRowValue = value)"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.AddedWhen)" Caption="Angelegt am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.ChangedWhen)" Caption="Geändert am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
@RenderColumns()
|
||||
</Columns>
|
||||
<EditFormTemplate Context="editFormContext">
|
||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
||||
@@ -230,6 +233,28 @@ else
|
||||
private string popupHeaderText = "Edit";
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
private IGrid? gridRef;
|
||||
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 List<ColumnDefinition> columnDefinitions = new()
|
||||
{
|
||||
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.CustomerName), Caption = "CustomerName", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.Amount), Caption = "Amount", DisplayFormat = "c2", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.Category), Caption = "Category", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.StatusFlag), Caption = "Status", ReadOnly = true, FilterType = ColumnFilterType.Bool },
|
||||
new() { FieldName = nameof(MassDataReadDto.AddedWhen), Caption = "Angelegt am", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
||||
new() { FieldName = nameof(MassDataReadDto.ChangedWhen), Caption = "Geändert am", ReadOnly = true, FilterType = ColumnFilterType.Date }
|
||||
};
|
||||
|
||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||
|
||||
private readonly List<PageSizeOption> pageSizeOptions = new()
|
||||
{
|
||||
@@ -254,6 +279,9 @@ else
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
await EnsureLayoutUserAsync();
|
||||
await LoadBandLayoutAsync();
|
||||
await LoadPage(0);
|
||||
}
|
||||
|
||||
@@ -293,6 +321,417 @@ 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();
|
||||
ApplyBandOrderingFromColumnOrder();
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private async Task SaveBandLayoutAsync()
|
||||
{
|
||||
await SaveGridLayoutAsync();
|
||||
}
|
||||
|
||||
private async Task SaveGridLayoutAsync()
|
||||
{
|
||||
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 = "Grid-Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Grid-Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (bandLayout.ColumnOrder.Count > 0)
|
||||
{
|
||||
var ordered = bandLayout.ColumnOrder
|
||||
.Where(columnLookup.ContainsKey)
|
||||
.Select(field => columnLookup[field])
|
||||
.ToList();
|
||||
|
||||
ordered.AddRange(columnDefinitions.Where(column => !ordered.Contains(column)));
|
||||
columnDefinitions = ordered;
|
||||
}
|
||||
|
||||
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 CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var gridColumns = gridRef.GetColumns()
|
||||
.OfType<IGridDataColumn>()
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnOrder = gridColumns
|
||||
.OrderBy(column => column.VisibleIndex)
|
||||
.Select(column => column.FieldName)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnWidths = gridColumns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
ApplyBandOrderingFromColumnOrder();
|
||||
}
|
||||
|
||||
private void ApplyBandOrderingFromColumnOrder()
|
||||
{
|
||||
if (bandLayout.ColumnOrder.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bandById = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
||||
var orderedBandIds = new List<string>();
|
||||
var orderedColumnsByBand = bandLayout.Bands.ToDictionary(
|
||||
band => band.Id,
|
||||
_ => new List<string>(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var field in bandLayout.ColumnOrder)
|
||||
{
|
||||
if (columnBandAssignments.TryGetValue(field, out var bandId) && bandById.ContainsKey(bandId))
|
||||
{
|
||||
if (!orderedBandIds.Contains(bandId, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
orderedBandIds.Add(bandId);
|
||||
}
|
||||
|
||||
orderedColumnsByBand[bandId].Add(field);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
var orderedColumns = orderedColumnsByBand[band.Id];
|
||||
orderedColumns.AddRange(band.Columns.Where(column => !orderedColumns.Contains(column, StringComparer.OrdinalIgnoreCase)));
|
||||
band.Columns = orderedColumns;
|
||||
}
|
||||
|
||||
if (orderedBandIds.Count > 0)
|
||||
{
|
||||
bandLayout.Bands = orderedBandIds
|
||||
.Select(id => bandById[id])
|
||||
.Concat(bandLayout.Bands.Where(band => !orderedBandIds.Contains(band.Id, StringComparer.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
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 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>();
|
||||
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 RenderFragment RenderColumns() => builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Width", "120px");
|
||||
builder.CloseComponent();
|
||||
|
||||
var bandLookup = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
||||
var renderedBands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var orderedFields = bandLayout.ColumnOrder
|
||||
.Where(columnLookup.ContainsKey)
|
||||
.ToList();
|
||||
|
||||
if (orderedFields.Count == 0)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var fieldName in orderedFields)
|
||||
{
|
||||
if (columnBandAssignments.TryGetValue(fieldName, out var bandId) && bandLookup.TryGetValue(bandId, out var band))
|
||||
{
|
||||
if (!renderedBands.Add(bandId) || 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();
|
||||
}
|
||||
else if (columnLookup.TryGetValue(fieldName, out var column))
|
||||
{
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var column in columnDefinitions)
|
||||
{
|
||||
if (!orderedFields.Contains(column.FieldName, StringComparer.OrdinalIgnoreCase) &&
|
||||
(!columnBandAssignments.TryGetValue(column.FieldName, out var bandId) || !bandLookup.ContainsKey(bandId)))
|
||||
{
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
if (renderedBands.Contains(band.Id) || 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)
|
||||
@@ -337,7 +776,7 @@ else
|
||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
}
|
||||
|
||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
{
|
||||
if (e.IsNew)
|
||||
{
|
||||
@@ -429,6 +868,43 @@ else
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
9
DbFirst.BlazorWasm/Models/LayoutDto.cs
Normal file
9
DbFirst.BlazorWasm/Models/LayoutDto.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace DbFirst.BlazorWasm.Models;
|
||||
|
||||
public class LayoutDto
|
||||
{
|
||||
public string LayoutType { get; set; } = string.Empty;
|
||||
public string LayoutKey { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string LayoutData { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -19,5 +19,6 @@ builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(apiBaseU
|
||||
builder.Services.AddScoped<CatalogApiClient>();
|
||||
builder.Services.AddScoped<DashboardApiClient>();
|
||||
builder.Services.AddScoped<MassDataApiClient>();
|
||||
builder.Services.AddScoped<LayoutApiClient>();
|
||||
|
||||
await builder.Build().RunAsync();
|
||||
|
||||
64
DbFirst.BlazorWasm/Services/LayoutApiClient.cs
Normal file
64
DbFirst.BlazorWasm/Services/LayoutApiClient.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Net.Http.Json;
|
||||
using DbFirst.BlazorWasm.Models;
|
||||
|
||||
namespace DbFirst.BlazorWasm.Services;
|
||||
|
||||
public class LayoutApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string Endpoint = "api/layouts";
|
||||
|
||||
public LayoutApiClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName)
|
||||
{
|
||||
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<LayoutDto>();
|
||||
}
|
||||
|
||||
public async Task<LayoutDto> UpsertAsync(LayoutDto dto)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var detail = await ReadErrorAsync(response);
|
||||
throw new InvalidOperationException(detail);
|
||||
}
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<LayoutDto>();
|
||||
return payload ?? dto;
|
||||
}
|
||||
|
||||
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
if (!string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return body;
|
||||
}
|
||||
|
||||
return $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string layoutType, string layoutKey, string userName)
|
||||
{
|
||||
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
||||
var response = await _httpClient.DeleteAsync(url);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,10 @@
|
||||
@using System.Text.Json
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Rendering
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@inject CatalogApiClient Api
|
||||
@inject LayoutApiClient LayoutApi
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
@@ -7,6 +12,25 @@
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
@@ -28,11 +52,45 @@ else if (items.Count == 0)
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="band-editor">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Band-Layout speichern" Click="SaveBandLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Grid-Layout speichern" Click="SaveGridLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||
</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 class="grid-section">
|
||||
<DxGrid Data="@items"
|
||||
TItem="CatalogReadDto"
|
||||
KeyFieldName="@nameof(CatalogReadDto.Guid)"
|
||||
ShowFilterRow="true"
|
||||
AllowColumnResize="true"
|
||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||
AllowColumnReorder="true"
|
||||
PageSize="10"
|
||||
CssClass="mb-4 catalog-grid"
|
||||
EditMode="GridEditMode.PopupEditForm"
|
||||
@@ -40,58 +98,10 @@ else
|
||||
PopupEditFormHeaderText="@popupHeaderText"
|
||||
CustomizeEditModel="OnCustomizeEditModel"
|
||||
EditModelSaving="OnEditModelSaving"
|
||||
DataItemDeleting="OnDataItemDeleting">
|
||||
DataItemDeleting="OnDataItemDeleting"
|
||||
@ref="gridRef">
|
||||
<Columns>
|
||||
<DxGridCommandColumn Width="120px" />
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.Guid)" Caption="Id" Width="140px" SortIndex="0" SortOrder="GridColumnSortOrder.Ascending">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatTitle)" Caption="Titel">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.CatString)" Caption="String">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWho)" Caption="Angelegt von" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.AddedWhen)" Caption="Angelegt am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWho)" Caption="Geändert von" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(CatalogReadDto.ChangedWhen)" Caption="Geändert am" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
@RenderColumns()
|
||||
</Columns>
|
||||
<EditFormTemplate Context="editFormContext">
|
||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (CatalogEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
||||
@@ -130,7 +140,27 @@ else
|
||||
private string? infoMessage;
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
private IGrid? gridRef;
|
||||
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 List<ColumnDefinition> columnDefinitions = new()
|
||||
{
|
||||
new() { FieldName = nameof(CatalogReadDto.Guid), Caption = "Id", Width = "140px", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(CatalogReadDto.CatTitle), Caption = "Titel", FilterType = ColumnFilterType.Text },
|
||||
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 }
|
||||
};
|
||||
|
||||
private readonly List<ProcedureOption> procedureOptions = new()
|
||||
{
|
||||
@@ -138,8 +168,13 @@ else
|
||||
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
|
||||
};
|
||||
|
||||
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
await EnsureLayoutUserAsync();
|
||||
await LoadBandLayoutAsync();
|
||||
await LoadCatalogs();
|
||||
}
|
||||
|
||||
@@ -349,6 +384,415 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
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 SaveBandLayoutAsync()
|
||||
{
|
||||
await SaveGridLayoutAsync();
|
||||
}
|
||||
|
||||
private async Task SaveGridLayoutAsync()
|
||||
{
|
||||
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 = "Grid-Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Grid-Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
ApplyBandOrderingFromColumnOrder();
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private void CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var gridColumns = gridRef.GetColumns()
|
||||
.OfType<IGridDataColumn>()
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnOrder = gridColumns
|
||||
.OrderBy(column => column.VisibleIndex)
|
||||
.Select(column => column.FieldName)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnWidths = gridColumns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
ApplyBandOrderingFromColumnOrder();
|
||||
}
|
||||
|
||||
private void ApplyBandOrderingFromColumnOrder()
|
||||
{
|
||||
if (bandLayout.ColumnOrder.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bandById = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
||||
var orderedBandIds = new List<string>();
|
||||
var orderedColumnsByBand = bandLayout.Bands.ToDictionary(
|
||||
band => band.Id,
|
||||
_ => new List<string>(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var field in bandLayout.ColumnOrder)
|
||||
{
|
||||
if (columnBandAssignments.TryGetValue(field, out var bandId) && bandById.ContainsKey(bandId))
|
||||
{
|
||||
if (!orderedBandIds.Contains(bandId, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
orderedBandIds.Add(bandId);
|
||||
}
|
||||
|
||||
orderedColumnsByBand[bandId].Add(field);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
var orderedColumns = orderedColumnsByBand[band.Id];
|
||||
orderedColumns.AddRange(band.Columns.Where(column => !orderedColumns.Contains(column, StringComparer.OrdinalIgnoreCase)));
|
||||
band.Columns = orderedColumns;
|
||||
}
|
||||
|
||||
if (orderedBandIds.Count > 0)
|
||||
{
|
||||
bandLayout.Bands = orderedBandIds
|
||||
.Select(id => bandById[id])
|
||||
.Concat(bandLayout.Bands.Where(band => !orderedBandIds.Contains(band.Id, StringComparer.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
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>();
|
||||
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 bandLookup = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
||||
var renderedBands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var orderedFields = bandLayout.ColumnOrder
|
||||
.Where(columnLookup.ContainsKey)
|
||||
.ToList();
|
||||
|
||||
if (orderedFields.Count == 0)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var fieldName in orderedFields)
|
||||
{
|
||||
if (columnBandAssignments.TryGetValue(fieldName, out var bandId) && bandLookup.TryGetValue(bandId, out var band))
|
||||
{
|
||||
if (!renderedBands.Add(bandId) || 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();
|
||||
}
|
||||
else if (columnLookup.TryGetValue(fieldName, out var column))
|
||||
{
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var column in columnDefinitions)
|
||||
{
|
||||
if (!orderedFields.Contains(column.FieldName, StringComparer.OrdinalIgnoreCase) &&
|
||||
(!columnBandAssignments.TryGetValue(column.FieldName, out var bandId) || !bandLookup.ContainsKey(bandId)))
|
||||
{
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
if (renderedBands.Contains(band.Id) || 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);
|
||||
}
|
||||
|
||||
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; }
|
||||
@@ -364,4 +808,42 @@ else
|
||||
public int Value { get; set; }
|
||||
public string Text { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (bandLayout.ColumnOrder.Count > 0)
|
||||
{
|
||||
var ordered = bandLayout.ColumnOrder
|
||||
.Where(columnLookup.ContainsKey)
|
||||
.Select(field => columnLookup[field])
|
||||
.ToList();
|
||||
|
||||
ordered.AddRange(columnDefinitions.Where(column => !ordered.Contains(column)));
|
||||
columnDefinitions = ordered;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
@using System.Text.Json
|
||||
@using Microsoft.AspNetCore.Components
|
||||
@using Microsoft.AspNetCore.Components.Rendering
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@inject MassDataApiClient Api
|
||||
@inject LayoutApiClient LayoutApi
|
||||
@inject IJSRuntime JsRuntime
|
||||
|
||||
<style>
|
||||
.action-panel { margin-bottom: 16px; }
|
||||
@@ -71,6 +76,25 @@
|
||||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
@if (!string.IsNullOrWhiteSpace(errorMessage))
|
||||
@@ -104,13 +128,45 @@ else
|
||||
CssClass="page-size-combo" />
|
||||
</div>
|
||||
|
||||
<div class="band-editor">
|
||||
<div class="band-controls">
|
||||
<DxButton Text="Band hinzufügen" Click="AddBand" />
|
||||
<DxButton Text="Band-Layout speichern" Click="SaveBandLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Grid-Layout speichern" Click="SaveGridLayoutAsync" Enabled="@CanSaveBandLayout" />
|
||||
<DxButton Text="Band-Layout zurücksetzen" Click="ResetBandLayoutAsync" />
|
||||
</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 class="grid-section">
|
||||
<DxGrid Data="@items"
|
||||
TItem="MassDataReadDto"
|
||||
KeyFieldName="@nameof(MassDataReadDto.Id)"
|
||||
ShowFilterRow="true"
|
||||
ShowGroupPanel="true"
|
||||
AllowColumnResize="true"
|
||||
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
|
||||
AllowColumnReorder="true"
|
||||
PagerVisible="false"
|
||||
PageSize="@(pageSize ?? 100)"
|
||||
CssClass="mb-3 massdata-grid"
|
||||
@@ -118,63 +174,10 @@ else
|
||||
PopupEditFormHeaderText="@popupHeaderText"
|
||||
CustomizeEditModel="OnCustomizeEditModel"
|
||||
EditModelSaving="OnEditModelSaving"
|
||||
DataItemDeleting="OnDataItemDeleting">
|
||||
DataItemDeleting="OnDataItemDeleting"
|
||||
@ref="gridRef">
|
||||
<Columns>
|
||||
<DxGridCommandColumn Width="120px" />
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Id)" Caption="Id" Width="90px" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.CustomerName)" Caption="CustomerName">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Amount)" Caption="Amount" DisplayFormat="c2">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue?.ToString())"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.Category)" Caption="Category" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxTextBox Text="@(filter.FilterRowValue as string)"
|
||||
TextChanged="@(value => filter.FilterRowValue = value)"
|
||||
CssClass="filter-search-input" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.StatusFlag)" Caption="Status" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxComboBox Data="@statusFilterOptions"
|
||||
TData="BoolFilterOption"
|
||||
TValue="bool?"
|
||||
TextFieldName="Text"
|
||||
ValueFieldName="Value"
|
||||
Value="@(filter.FilterRowValue as bool?)"
|
||||
ValueChanged="@(value => filter.FilterRowValue = value)"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.AddedWhen)" Caption="Added" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
<DxGridDataColumn FieldName="@nameof(MassDataReadDto.ChangedWhen)" Caption="Changed" ReadOnly="true">
|
||||
<FilterRowCellTemplate Context="filter">
|
||||
<DxDateEdit Date="@((DateTime?)filter.FilterRowValue)"
|
||||
DateChanged="@((DateTime? value) => { filter.FilterRowValue = value; })"
|
||||
Width="100%" />
|
||||
</FilterRowCellTemplate>
|
||||
</DxGridDataColumn>
|
||||
@RenderColumns()
|
||||
</Columns>
|
||||
<EditFormTemplate Context="editFormContext">
|
||||
@{ SetEditContext(editFormContext.EditContext); var editModel = (MassDataEditModel)editFormContext.EditModel; SetPopupHeaderText(editModel.IsNew); }
|
||||
@@ -230,6 +233,37 @@ else
|
||||
private string popupHeaderText = "Edit";
|
||||
private EditContext? editContext;
|
||||
private ValidationMessageStore? validationMessageStore;
|
||||
private IGrid? gridRef;
|
||||
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 List<ColumnDefinition> columnDefinitions = new()
|
||||
{
|
||||
new() { FieldName = nameof(MassDataReadDto.Id), Caption = "Id", Width = "90px", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.CustomerName), Caption = "CustomerName", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.Amount), Caption = "Amount", DisplayFormat = "c2", FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.Category), Caption = "Category", ReadOnly = true, FilterType = ColumnFilterType.Text },
|
||||
new() { FieldName = nameof(MassDataReadDto.StatusFlag), Caption = "Status", ReadOnly = true, FilterType = ColumnFilterType.Bool },
|
||||
new() { FieldName = nameof(MassDataReadDto.AddedWhen), Caption = "Added", ReadOnly = true, FilterType = ColumnFilterType.Date },
|
||||
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 = 100000, Text = "100.000" },
|
||||
new() { Value = null, Text = "Alle" }
|
||||
};
|
||||
|
||||
private readonly List<BoolFilterOption> statusFilterOptions = new()
|
||||
{
|
||||
@@ -243,17 +277,11 @@ else
|
||||
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
|
||||
};
|
||||
|
||||
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 = 100000, Text = "100.000" },
|
||||
new() { Value = null, Text = "Alle" }
|
||||
};
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
|
||||
await EnsureLayoutUserAsync();
|
||||
await LoadBandLayoutAsync();
|
||||
await LoadPage(0);
|
||||
}
|
||||
|
||||
@@ -293,6 +321,417 @@ 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();
|
||||
ApplyBandOrderingFromColumnOrder();
|
||||
UpdateBandOptions();
|
||||
}
|
||||
|
||||
private async Task SaveBandLayoutAsync()
|
||||
{
|
||||
await SaveGridLayoutAsync();
|
||||
}
|
||||
|
||||
private async Task SaveGridLayoutAsync()
|
||||
{
|
||||
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 = "Grid-Layout gespeichert.";
|
||||
errorMessage = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = $"Grid-Layout konnte nicht gespeichert werden: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
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 CaptureColumnLayoutFromGrid()
|
||||
{
|
||||
if (gridRef == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var gridColumns = gridRef.GetColumns()
|
||||
.OfType<IGridDataColumn>()
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.FieldName))
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnOrder = gridColumns
|
||||
.OrderBy(column => column.VisibleIndex)
|
||||
.Select(column => column.FieldName)
|
||||
.ToList();
|
||||
|
||||
bandLayout.ColumnWidths = gridColumns
|
||||
.Where(column => !string.IsNullOrWhiteSpace(column.Width))
|
||||
.ToDictionary(column => column.FieldName, column => column.Width, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
ApplyBandOrderingFromColumnOrder();
|
||||
}
|
||||
|
||||
private void ApplyBandOrderingFromColumnOrder()
|
||||
{
|
||||
if (bandLayout.ColumnOrder.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bandById = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
||||
var orderedBandIds = new List<string>();
|
||||
var orderedColumnsByBand = bandLayout.Bands.ToDictionary(
|
||||
band => band.Id,
|
||||
_ => new List<string>(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var field in bandLayout.ColumnOrder)
|
||||
{
|
||||
if (columnBandAssignments.TryGetValue(field, out var bandId) && bandById.ContainsKey(bandId))
|
||||
{
|
||||
if (!orderedBandIds.Contains(bandId, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
orderedBandIds.Add(bandId);
|
||||
}
|
||||
|
||||
orderedColumnsByBand[bandId].Add(field);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
var orderedColumns = orderedColumnsByBand[band.Id];
|
||||
orderedColumns.AddRange(band.Columns.Where(column => !orderedColumns.Contains(column, StringComparer.OrdinalIgnoreCase)));
|
||||
band.Columns = orderedColumns;
|
||||
}
|
||||
|
||||
if (orderedBandIds.Count > 0)
|
||||
{
|
||||
bandLayout.Bands = orderedBandIds
|
||||
.Select(id => bandById[id])
|
||||
.Concat(bandLayout.Bands.Where(band => !orderedBandIds.Contains(band.Id, StringComparer.OrdinalIgnoreCase)))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyColumnLayoutFromStorage()
|
||||
{
|
||||
if (bandLayout.ColumnOrder.Count > 0)
|
||||
{
|
||||
var ordered = bandLayout.ColumnOrder
|
||||
.Where(columnLookup.ContainsKey)
|
||||
.Select(field => columnLookup[field])
|
||||
.ToList();
|
||||
|
||||
ordered.AddRange(columnDefinitions.Where(column => !ordered.Contains(column)));
|
||||
columnDefinitions = ordered;
|
||||
}
|
||||
|
||||
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 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>();
|
||||
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 RenderFragment RenderColumns() => builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
builder.OpenComponent<DxGridCommandColumn>(seq++);
|
||||
builder.AddAttribute(seq++, "Width", "120px");
|
||||
builder.CloseComponent();
|
||||
|
||||
var bandLookup = bandLayout.Bands.ToDictionary(band => band.Id, StringComparer.OrdinalIgnoreCase);
|
||||
var renderedBands = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var orderedFields = bandLayout.ColumnOrder
|
||||
.Where(columnLookup.ContainsKey)
|
||||
.ToList();
|
||||
|
||||
if (orderedFields.Count == 0)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var fieldName in orderedFields)
|
||||
{
|
||||
if (columnBandAssignments.TryGetValue(fieldName, out var bandId) && bandLookup.TryGetValue(bandId, out var band))
|
||||
{
|
||||
if (!renderedBands.Add(bandId) || 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();
|
||||
}
|
||||
else if (columnLookup.TryGetValue(fieldName, out var column))
|
||||
{
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var column in columnDefinitions)
|
||||
{
|
||||
if (!orderedFields.Contains(column.FieldName, StringComparer.OrdinalIgnoreCase) &&
|
||||
(!columnBandAssignments.TryGetValue(column.FieldName, out var bandId) || !bandLookup.ContainsKey(bandId)))
|
||||
{
|
||||
BuildDataColumn(builder, ref seq, column);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var band in bandLayout.Bands)
|
||||
{
|
||||
if (renderedBands.Contains(band.Id) || 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)
|
||||
@@ -337,7 +776,7 @@ else
|
||||
popupHeaderText = isNew ? "Neu" : "Edit";
|
||||
}
|
||||
|
||||
private void OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
private async Task OnCustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
{
|
||||
if (e.IsNew)
|
||||
{
|
||||
@@ -429,6 +868,43 @@ else
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
9
DbFirst.BlazorWebApp/Models/LayoutDto.cs
Normal file
9
DbFirst.BlazorWebApp/Models/LayoutDto.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace DbFirst.BlazorWebApp.Models;
|
||||
|
||||
public class LayoutDto
|
||||
{
|
||||
public string LayoutType { get; set; } = string.Empty;
|
||||
public string LayoutKey { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public string LayoutData { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -25,12 +25,17 @@ if (!string.IsNullOrWhiteSpace(apiBaseUrl))
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
builder.Services.AddHttpClient<LayoutApiClient>(client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Services.AddHttpClient<CatalogApiClient>();
|
||||
builder.Services.AddHttpClient<DashboardApiClient>();
|
||||
builder.Services.AddHttpClient<MassDataApiClient>();
|
||||
builder.Services.AddHttpClient<LayoutApiClient>();
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
64
DbFirst.BlazorWebApp/Services/LayoutApiClient.cs
Normal file
64
DbFirst.BlazorWebApp/Services/LayoutApiClient.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System.Net.Http.Json;
|
||||
using DbFirst.BlazorWebApp.Models;
|
||||
|
||||
namespace DbFirst.BlazorWebApp.Services;
|
||||
|
||||
public class LayoutApiClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string Endpoint = "api/layouts";
|
||||
|
||||
public LayoutApiClient(HttpClient httpClient)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
}
|
||||
|
||||
public async Task<LayoutDto?> GetAsync(string layoutType, string layoutKey, string userName)
|
||||
{
|
||||
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
||||
var response = await _httpClient.GetAsync(url);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<LayoutDto>();
|
||||
}
|
||||
|
||||
public async Task<LayoutDto> UpsertAsync(LayoutDto dto)
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync(Endpoint, dto);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var detail = await ReadErrorAsync(response);
|
||||
throw new InvalidOperationException(detail);
|
||||
}
|
||||
|
||||
var payload = await response.Content.ReadFromJsonAsync<LayoutDto>();
|
||||
return payload ?? dto;
|
||||
}
|
||||
|
||||
private static async Task<string> ReadErrorAsync(HttpResponseMessage response)
|
||||
{
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
if (!string.IsNullOrWhiteSpace(body))
|
||||
{
|
||||
return body;
|
||||
}
|
||||
|
||||
return $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim();
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(string layoutType, string layoutKey, string userName)
|
||||
{
|
||||
var url = $"{Endpoint}?layoutType={Uri.EscapeDataString(layoutType)}&layoutKey={Uri.EscapeDataString(layoutKey)}&userName={Uri.EscapeDataString(userName)}";
|
||||
var response = await _httpClient.DeleteAsync(url);
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
15
DbFirst.Domain/Entities/SmfLayout.cs
Normal file
15
DbFirst.Domain/Entities/SmfLayout.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace DbFirst.Domain.Entities;
|
||||
|
||||
public class SmfLayout
|
||||
{
|
||||
public long Guid { get; set; }
|
||||
public bool Active { get; set; }
|
||||
public string LayoutType { get; set; } = string.Empty;
|
||||
public string LayoutKey { get; set; } = string.Empty;
|
||||
public string UserName { get; set; } = string.Empty;
|
||||
public byte[] LayoutData { get; set; } = Array.Empty<byte>();
|
||||
public string AddedWho { get; set; } = string.Empty;
|
||||
public DateTime AddedWhen { get; set; }
|
||||
public string? ChangedWho { get; set; }
|
||||
public DateTime? ChangedWhen { get; set; }
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public partial class ApplicationDbContext : DbContext
|
||||
}
|
||||
|
||||
public virtual DbSet<VwmyCatalog> VwmyCatalogs { get; set; }
|
||||
public virtual DbSet<SmfLayout> SmfLayouts { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -51,6 +52,37 @@ public partial class ApplicationDbContext : DbContext
|
||||
.HasColumnName(catCfg.ChangedWhoColumnName);
|
||||
});
|
||||
|
||||
modelBuilder.Entity<SmfLayout>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Guid);
|
||||
entity.ToTable("TBDD_SMF_LAYOUT", tb => tb.HasTrigger("TBDD_SMF_LAYOUT_AFT_UPD"));
|
||||
|
||||
entity.Property(e => e.Guid).HasColumnName("GUID");
|
||||
entity.Property(e => e.Active).HasColumnName("ACTIVE");
|
||||
entity.Property(e => e.LayoutType)
|
||||
.HasMaxLength(50)
|
||||
.HasColumnName("LAYOUT_TYPE");
|
||||
entity.Property(e => e.LayoutKey)
|
||||
.HasMaxLength(150)
|
||||
.HasColumnName("LAYOUT_KEY");
|
||||
entity.Property(e => e.UserName)
|
||||
.HasMaxLength(50)
|
||||
.HasColumnName("USER_NAME");
|
||||
entity.Property(e => e.LayoutData).HasColumnName("LAYOUT_DATA");
|
||||
entity.Property(e => e.AddedWho)
|
||||
.HasMaxLength(50)
|
||||
.HasColumnName("ADDED_WHO");
|
||||
entity.Property(e => e.AddedWhen)
|
||||
.HasColumnType("datetime")
|
||||
.HasColumnName("ADDED_WHEN");
|
||||
entity.Property(e => e.ChangedWho)
|
||||
.HasMaxLength(50)
|
||||
.HasColumnName("CHANGED_WHO");
|
||||
entity.Property(e => e.ChangedWhen)
|
||||
.HasColumnType("datetime")
|
||||
.HasColumnName("CHANGED_WHEN");
|
||||
});
|
||||
|
||||
OnModelCreatingPartial(modelBuilder);
|
||||
}
|
||||
|
||||
|
||||
66
DbFirst.Infrastructure/Repositories/LayoutRepository.cs
Normal file
66
DbFirst.Infrastructure/Repositories/LayoutRepository.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using DbFirst.Application.Repositories;
|
||||
using DbFirst.Domain.Entities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace DbFirst.Infrastructure.Repositories;
|
||||
|
||||
public class LayoutRepository : ILayoutRepository
|
||||
{
|
||||
private readonly ApplicationDbContext _db;
|
||||
|
||||
public LayoutRepository(ApplicationDbContext db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<SmfLayout?> GetAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _db.SmfLayouts.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.LayoutType == layoutType && x.LayoutKey == layoutKey && x.UserName == userName, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<SmfLayout> UpsertAsync(string layoutType, string layoutKey, string userName, byte[] layoutData, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await _db.SmfLayouts
|
||||
.FirstOrDefaultAsync(x => x.LayoutType == layoutType && x.LayoutKey == layoutKey && x.UserName == userName, cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
entity = new SmfLayout
|
||||
{
|
||||
Active = true,
|
||||
LayoutType = layoutType,
|
||||
LayoutKey = layoutKey,
|
||||
UserName = userName,
|
||||
LayoutData = layoutData,
|
||||
AddedWho = userName,
|
||||
AddedWhen = DateTime.Now
|
||||
};
|
||||
_db.SmfLayouts.Add(entity);
|
||||
}
|
||||
else
|
||||
{
|
||||
entity.Active = true;
|
||||
entity.LayoutData = layoutData;
|
||||
entity.ChangedWho = userName;
|
||||
}
|
||||
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return entity;
|
||||
}
|
||||
|
||||
public async Task<bool> DeleteAsync(string layoutType, string layoutKey, string userName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await _db.SmfLayouts
|
||||
.FirstOrDefaultAsync(x => x.LayoutType == layoutType && x.LayoutKey == layoutKey && x.UserName == userName, cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_db.SmfLayouts.Remove(entity);
|
||||
await _db.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user