Compare commits

...

4 Commits

Author SHA1 Message Date
OlgunR
8387b71676 Add operator selection to grid filter rows for dates/numbers
Introduce custom filter row cell templates for date and numeric columns in CatalogsGrid and MassDataGrid. Users can now select a filter operator (e.g., =, <, >, <=, >=) alongside the filter value for columns like AddedWhen, ChangedWhen, and Amount. Updated filter criteria logic to support these operators using DevExpress expressions. Added new CSS for filter row alignment and appearance. Removed obsolete methods and improved layout logic. This enhances filtering flexibility and user experience in both grids.
2026-02-09 16:10:16 +01:00
OlgunR
8824492057 Enable grouping features in DxGrid components
Added ShowGroupPanel, ShowGroupedColumns, and AllowGroup to DxGrid in CatalogsGrid.razor and MassDataGrid.razor for enhanced data grouping. Also set PagerVisible="false" in MassDataGrid.razor to hide pager controls.
2026-02-09 14:03:47 +01:00
OlgunR
59f22be405 Update nav menu with new custom SVG icons
Replaced icons for "Catalogs", "Dashboards", and "MassData" in NavMenu.razor with new custom Bootstrap-style SVG icons. Added corresponding CSS classes using data URIs to embed the new white icons, ensuring consistent appearance in the navigation menu.
2026-02-09 13:41:11 +01:00
OlgunR
23865aefb6 Remove Counter and Weather pages and nav links
Removed the Counter and Weather pages by deleting their component files. Also updated the navigation menu to remove links to these pages, leaving only Home and Catalogs. This simplifies the app's navigation and available features.
2026-02-09 13:22:04 +01:00
10 changed files with 705 additions and 231 deletions

View File

@@ -3,6 +3,7 @@
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Forms
@using DevExpress.Blazor
@using DevExpress.Data.Filtering
@inject CatalogApiClient Api
@inject LayoutApiClient LayoutApi
@inject IJSRuntime JsRuntime
@@ -73,6 +74,21 @@
.band-columns {
max-width: 720px;
}
.filter-row-cell {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.filter-operator {
width: 52px;
min-width: 52px;
flex: 0 0 52px;
}
.filter-value {
min-width: 140px;
flex: 1 1 140px;
}
</style>
@if (!string.IsNullOrWhiteSpace(errorMessage))
@@ -128,6 +144,9 @@ else
<DxGrid Data="@items"
TItem="CatalogReadDto"
KeyFieldName="@nameof(CatalogReadDto.Guid)"
ShowGroupPanel="true"
ShowGroupedColumns="true"
AllowGroup="true"
ShowFilterRow="true"
AllowColumnResize="true"
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
@@ -183,6 +202,10 @@ else
private ValidationMessageStore? validationMessageStore;
private IGrid? gridRef;
private string popupHeaderText = "Edit";
private DateTime? addedWhenFilterValue;
private string addedWhenFilterOperator = "=";
private DateTime? changedWhenFilterValue;
private string changedWhenFilterOperator = "=";
private const string LayoutType = "GRID_BANDS";
private const string LayoutKey = "CatalogsGrid";
private const string LayoutUserStorageKey = "layoutUser";
@@ -209,8 +232,16 @@ else
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
};
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private readonly List<FilterOperatorOption> filterOperators = new()
{
new() { Value = "=", Text = "=" },
new() { Value = "<", Text = "<" },
new() { Value = ">", Text = ">" },
new() { Value = "<=", Text = "<=" },
new() { Value = ">=", Text = ">=" }
};
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private bool gridLayoutApplied;
protected override async Task OnInitializedAsync()
@@ -551,49 +582,6 @@ else
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
}
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
@@ -757,22 +745,95 @@ else
builder.AddAttribute(seq++, "ReadOnly", true);
}
if (string.Equals(column.FieldName, nameof(CatalogReadDto.AddedWhen), StringComparison.OrdinalIgnoreCase))
{
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildAddedWhenFilterTemplate());
}
else if (string.Equals(column.FieldName, nameof(CatalogReadDto.ChangedWhen), StringComparison.OrdinalIgnoreCase))
{
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildChangedWhenFilterTemplate());
}
builder.CloseComponent();
}
private RenderFragment? BuildFilterTemplate(ColumnDefinition column)
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildAddedWhenFilterTemplate()
{
return null;
return BuildDateFilterTemplate(
() => addedWhenFilterValue,
value => addedWhenFilterValue = value,
() => addedWhenFilterOperator,
value => addedWhenFilterOperator = value);
}
private RenderFragment? BuildTextFilterTemplate()
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildChangedWhenFilterTemplate()
{
return null;
return BuildDateFilterTemplate(
() => changedWhenFilterValue,
value => changedWhenFilterValue = value,
() => changedWhenFilterOperator,
value => changedWhenFilterOperator = value);
}
private RenderFragment? BuildDateFilterTemplate()
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildDateFilterTemplate(
Func<DateTime?> getValue,
Action<DateTime?> setValue,
Func<string> getOperator,
Action<string> setOperator)
{
return null;
return context => builder =>
{
var seq = 0;
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", "filter-row-cell");
builder.OpenComponent<DxComboBox<FilterOperatorOption, string>>(seq++);
builder.AddAttribute(seq++, "Data", filterOperators);
builder.AddAttribute(seq++, "TextFieldName", "Text");
builder.AddAttribute(seq++, "ValueFieldName", "Value");
builder.AddAttribute(seq++, "Value", getOperator());
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<string>(this, value =>
{
setOperator(value);
UpdateFilterCriteria(context, value, getValue());
}));
builder.AddAttribute(seq++, "CssClass", "filter-operator");
builder.CloseComponent();
builder.OpenComponent<DxDateEdit<DateTime?>>(seq++);
builder.AddAttribute(seq++, "Date", getValue());
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateTime?>(this, value =>
{
setValue(value);
UpdateFilterCriteria(context, getOperator(), value);
}));
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.AddAttribute(seq++, "CssClass", "filter-value");
builder.CloseComponent();
builder.CloseElement();
};
}
private void UpdateFilterCriteria(GridDataColumnFilterRowCellTemplateContext context, string op, DateTime? value)
{
if (!value.HasValue)
{
context.FilterCriteria = null;
return;
}
var prop = new OperandProperty(context.DataColumn.FieldName);
var date = value.Value.Date;
context.FilterCriteria = op switch
{
"=" => new GroupOperator(GroupOperatorType.And, prop >= date, prop < date.AddDays(1)),
"<" => prop < date,
"<=" => prop < date.AddDays(1),
">" => prop >= date.AddDays(1),
">=" => prop >= date,
_ => prop == date
};
}
private sealed class BandLayout
@@ -827,4 +888,10 @@ else
public int Value { get; set; }
public string Text { get; set; } = string.Empty;
}
private sealed class FilterOperatorOption
{
public string Value { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
}
}

View File

@@ -3,6 +3,7 @@
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Forms
@using DevExpress.Blazor
@using DevExpress.Data.Filtering
@inject MassDataApiClient Api
@inject LayoutApiClient LayoutApi
@inject IJSRuntime JsRuntime
@@ -96,6 +97,25 @@
.band-columns {
max-width: 720px;
}
.filter-row-cell {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.filter-operator {
width: 52px;
min-width: 52px;
flex: 0 0 52px;
}
.filter-value {
min-width: 140px;
flex: 1 1 140px;
}
.filter-value-amount {
min-width: 110px;
flex-basis: 110px;
}
</style>
@if (!string.IsNullOrWhiteSpace(errorMessage))
@@ -163,6 +183,9 @@ else
<DxGrid Data="@items"
TItem="MassDataReadDto"
KeyFieldName="@nameof(MassDataReadDto.Id)"
ShowGroupPanel="true"
ShowGroupedColumns="true"
AllowGroup="true"
ShowFilterRow="true"
AllowColumnResize="true"
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
@@ -231,6 +254,12 @@ else
private int pageCount = 1;
private int? pageSize = 100;
private string popupHeaderText = "Edit";
private decimal? amountFilterValue;
private string amountFilterOperator = "=";
private DateTime? addedWhenFilterValue;
private string addedWhenFilterOperator = "=";
private DateTime? changedWhenFilterValue;
private string changedWhenFilterOperator = "=";
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private IGrid? gridRef;
@@ -277,6 +306,17 @@ else
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
};
private readonly List<FilterOperatorOption> filterOperators = new()
{
new() { Value = "=", Text = "=" },
new() { Value = "<", Text = "<" },
new() { Value = ">", Text = ">" },
new() { Value = "<=", Text = "<=" },
new() { Value = ">=", Text = ">=" }
};
private bool gridLayoutApplied;
protected override async Task OnInitializedAsync()
{
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
@@ -598,9 +638,163 @@ else
builder.AddAttribute(seq++, "ReadOnly", true);
}
if (string.Equals(column.FieldName, nameof(MassDataReadDto.Amount), StringComparison.OrdinalIgnoreCase))
{
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildAmountFilterTemplate());
}
else if (string.Equals(column.FieldName, nameof(MassDataReadDto.AddedWhen), StringComparison.OrdinalIgnoreCase))
{
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildAddedWhenFilterTemplate());
}
else if (string.Equals(column.FieldName, nameof(MassDataReadDto.ChangedWhen), StringComparison.OrdinalIgnoreCase))
{
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildChangedWhenFilterTemplate());
}
builder.CloseComponent();
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildAmountFilterTemplate()
{
return BuildNumberFilterTemplate(
() => amountFilterValue,
value => amountFilterValue = value,
() => amountFilterOperator,
value => amountFilterOperator = value);
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildAddedWhenFilterTemplate()
{
return BuildDateFilterTemplate(
() => addedWhenFilterValue,
value => addedWhenFilterValue = value,
() => addedWhenFilterOperator,
value => addedWhenFilterOperator = value);
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildChangedWhenFilterTemplate()
{
return BuildDateFilterTemplate(
() => changedWhenFilterValue,
value => changedWhenFilterValue = value,
() => changedWhenFilterOperator,
value => changedWhenFilterOperator = value);
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildNumberFilterTemplate(
Func<decimal?> getValue,
Action<decimal?> setValue,
Func<string> getOperator,
Action<string> setOperator)
{
return context => builder =>
{
var seq = 0;
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", "filter-row-cell");
builder.OpenComponent<DxComboBox<FilterOperatorOption, string>>(seq++);
builder.AddAttribute(seq++, "Data", filterOperators);
builder.AddAttribute(seq++, "TextFieldName", "Text");
builder.AddAttribute(seq++, "ValueFieldName", "Value");
builder.AddAttribute(seq++, "Value", getOperator());
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<string>(this, value =>
{
setOperator(value);
UpdateFilterCriteria(context, value, getValue());
}));
builder.AddAttribute(seq++, "CssClass", "filter-operator");
builder.CloseComponent();
builder.OpenComponent<DxSpinEdit<decimal?>>(seq++);
builder.AddAttribute(seq++, "Value", getValue());
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<decimal?>(this, value =>
{
setValue(value);
UpdateFilterCriteria(context, getOperator(), value);
}));
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.AddAttribute(seq++, "CssClass", "filter-value filter-value-amount");
builder.CloseComponent();
builder.CloseElement();
};
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildDateFilterTemplate(
Func<DateTime?> getValue,
Action<DateTime?> setValue,
Func<string> getOperator,
Action<string> setOperator)
{
return context => builder =>
{
var seq = 0;
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", "filter-row-cell");
builder.OpenComponent<DxComboBox<FilterOperatorOption, string>>(seq++);
builder.AddAttribute(seq++, "Data", filterOperators);
builder.AddAttribute(seq++, "TextFieldName", "Text");
builder.AddAttribute(seq++, "ValueFieldName", "Value");
builder.AddAttribute(seq++, "Value", getOperator());
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<string>(this, value =>
{
setOperator(value);
UpdateFilterCriteria(context, value, getValue());
}));
builder.AddAttribute(seq++, "CssClass", "filter-operator");
builder.CloseComponent();
builder.OpenComponent<DxDateEdit<DateTime?>>(seq++);
builder.AddAttribute(seq++, "Date", getValue());
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateTime?>(this, value =>
{
setValue(value);
UpdateFilterCriteria(context, getOperator(), value);
}));
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.AddAttribute(seq++, "CssClass", "filter-value");
builder.CloseComponent();
builder.CloseElement();
};
}
private void UpdateFilterCriteria(GridDataColumnFilterRowCellTemplateContext context, string op, object? value)
{
if (value == null)
{
context.FilterCriteria = null;
return;
}
var prop = new OperandProperty(context.DataColumn.FieldName);
if (value is DateTime dateValue)
{
var date = dateValue.Date;
context.FilterCriteria = op switch
{
"=" => new GroupOperator(GroupOperatorType.And, prop >= date, prop < date.AddDays(1)),
"<" => prop < date,
"<=" => prop < date.AddDays(1),
">" => prop >= date.AddDays(1),
">=" => prop >= date,
_ => prop == date
};
return;
}
context.FilterCriteria = op switch
{
"<" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Less),
">" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Greater),
"<=" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.LessOrEqual),
">=" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.GreaterOrEqual),
_ => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Equal)
};
}
private void SetEditContext(EditContext context)
{
if (editContext == context)
@@ -805,7 +999,11 @@ else
public string Text { get; set; } = string.Empty;
}
private bool gridLayoutApplied;
private sealed class FilterOperatorOption
{
public string Value { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{

View File

@@ -23,12 +23,12 @@
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="dashboards">
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
<span class="bi bi-speedometer-nav-menu" aria-hidden="true"></span> Dashboards
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="massdata">
<span class="bi bi-table" aria-hidden="true"></span> MassData
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
</NavLink>
</div>
</nav>

View File

@@ -33,6 +33,18 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.bi-collection-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M2 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 .5.5v1H2V3z'/%3E%3Cpath d='M2 5h12v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5z'/%3E%3C/svg%3E");
}
.bi-speedometer-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 11a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v4zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v8zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v2z'/%3E%3C/svg%3E");
}
.bi-table-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2zm1 1v2h12V3H2zm12 3H2v2h12V6zm0 3H2v2h12V9zm0 3H2v1h12v-1z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;

View File

@@ -3,6 +3,7 @@
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Forms
@using DevExpress.Blazor
@using DevExpress.Data.Filtering
@inject CatalogApiClient Api
@inject LayoutApiClient LayoutApi
@inject IJSRuntime JsRuntime
@@ -32,6 +33,21 @@
.band-columns {
max-width: 720px;
}
.filter-row-cell {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.filter-operator {
width: 52px;
min-width: 52px;
flex: 0 0 52px;
}
.filter-value {
min-width: 140px;
flex: 1 1 140px;
}
</style>
@if (!string.IsNullOrWhiteSpace(errorMessage))
@@ -87,6 +103,9 @@ else
<DxGrid Data="@items"
TItem="CatalogReadDto"
KeyFieldName="@nameof(CatalogReadDto.Guid)"
ShowGroupPanel="true"
ShowGroupedColumns="true"
AllowGroup="true"
ShowFilterRow="true"
AllowColumnResize="true"
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
@@ -142,6 +161,10 @@ else
private ValidationMessageStore? validationMessageStore;
private IGrid? gridRef;
private string popupHeaderText = "Edit";
private DateTime? addedWhenFilterValue;
private string addedWhenFilterOperator = "=";
private DateTime? changedWhenFilterValue;
private string changedWhenFilterOperator = "=";
private const string LayoutType = "GRID_BANDS";
private const string LayoutKey = "CatalogsGrid";
private const string LayoutUserStorageKey = "layoutUser";
@@ -168,7 +191,17 @@ else
new() { Value = 1, Text = "PRTBMY_CATALOG_SAVE" }
};
private readonly List<FilterOperatorOption> filterOperators = new()
{
new() { Value = "=", Text = "=" },
new() { Value = "<", Text = "<" },
new() { Value = ">", Text = ">" },
new() { Value = "<=", Text = "<=" },
new() { Value = ">=", Text = ">=" }
};
private bool CanSaveBandLayout => !string.IsNullOrWhiteSpace(layoutUser);
private bool gridLayoutApplied;
protected override async Task OnInitializedAsync()
{
@@ -178,6 +211,16 @@ else
await LoadCatalogs();
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
}
private void SetEditContext(EditContext context)
{
if (editContext == context)
@@ -471,6 +514,33 @@ else
UpdateBandOptions();
}
private async Task ResetBandLayoutAsync()
{
if (string.IsNullOrWhiteSpace(layoutUser))
{
return;
}
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout();
columnBandAssignments.Clear();
UpdateBandOptions();
infoMessage = "Band-Layout zurückgesetzt.";
}
private void ApplyColumnLayoutFromStorage()
{
foreach (var column in columnDefinitions)
{
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
{
column.Width = width;
}
}
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
}
private void AddBand()
{
bandLayout.Bands.Add(new BandDefinition
@@ -634,9 +704,97 @@ else
builder.AddAttribute(seq++, "ReadOnly", true);
}
if (string.Equals(column.FieldName, nameof(CatalogReadDto.AddedWhen), StringComparison.OrdinalIgnoreCase))
{
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildAddedWhenFilterTemplate());
}
else if (string.Equals(column.FieldName, nameof(CatalogReadDto.ChangedWhen), StringComparison.OrdinalIgnoreCase))
{
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildChangedWhenFilterTemplate());
}
builder.CloseComponent();
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildAddedWhenFilterTemplate()
{
return BuildDateFilterTemplate(
() => addedWhenFilterValue,
value => addedWhenFilterValue = value,
() => addedWhenFilterOperator,
value => addedWhenFilterOperator = value);
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildChangedWhenFilterTemplate()
{
return BuildDateFilterTemplate(
() => changedWhenFilterValue,
value => changedWhenFilterValue = value,
() => changedWhenFilterOperator,
value => changedWhenFilterOperator = value);
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildDateFilterTemplate(
Func<DateTime?> getValue,
Action<DateTime?> setValue,
Func<string> getOperator,
Action<string> setOperator)
{
return context => builder =>
{
var seq = 0;
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", "filter-row-cell");
builder.OpenComponent<DxComboBox<FilterOperatorOption, string>>(seq++);
builder.AddAttribute(seq++, "Data", filterOperators);
builder.AddAttribute(seq++, "TextFieldName", "Text");
builder.AddAttribute(seq++, "ValueFieldName", "Value");
builder.AddAttribute(seq++, "Value", getOperator());
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<string>(this, value =>
{
setOperator(value);
UpdateFilterCriteria(context, value, getValue());
}));
builder.AddAttribute(seq++, "CssClass", "filter-operator");
builder.CloseComponent();
builder.OpenComponent<DxDateEdit<DateTime?>>(seq++);
builder.AddAttribute(seq++, "Date", getValue());
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateTime?>(this, value =>
{
setValue(value);
UpdateFilterCriteria(context, getOperator(), value);
}));
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.AddAttribute(seq++, "CssClass", "filter-value");
builder.CloseComponent();
builder.CloseElement();
};
}
private void UpdateFilterCriteria(GridDataColumnFilterRowCellTemplateContext context, string op, DateTime? value)
{
if (!value.HasValue)
{
context.FilterCriteria = null;
return;
}
var prop = new OperandProperty(context.DataColumn.FieldName);
var date = value.Value.Date;
context.FilterCriteria = op switch
{
"=" => new GroupOperator(GroupOperatorType.And, prop >= date, prop < date.AddDays(1)),
"<" => prop < date,
"<=" => prop < date.AddDays(1),
">" => prop >= date.AddDays(1),
">=" => prop >= date,
_ => prop == date
};
}
private sealed class BandLayout
{
public List<BandDefinition> Bands { get; set; } = new();
@@ -690,42 +848,9 @@ else
public string Text { get; set; } = string.Empty;
}
private async Task ResetBandLayoutAsync()
private sealed class FilterOperatorOption
{
if (string.IsNullOrWhiteSpace(layoutUser))
{
return;
}
await LayoutApi.DeleteAsync(LayoutType, LayoutKey, layoutUser);
bandLayout = new BandLayout();
columnBandAssignments.Clear();
UpdateBandOptions();
infoMessage = "Band-Layout zurückgesetzt.";
}
private void ApplyColumnLayoutFromStorage()
{
foreach (var column in columnDefinitions)
{
if (bandLayout.ColumnWidths.TryGetValue(column.FieldName, out var width) && !string.IsNullOrWhiteSpace(width))
{
column.Width = width;
}
}
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
}
private bool gridLayoutApplied;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!gridLayoutApplied && gridRef != null && bandLayout.GridLayout != null)
{
gridRef.LoadLayout(bandLayout.GridLayout);
gridLayoutApplied = true;
await InvokeAsync(StateHasChanged);
}
public string Value { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
}
}

View File

@@ -14,17 +14,6 @@
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="counter">
<span class="bi bi-plus-square-fill-nav-menu" aria-hidden="true"></span> Counter
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="weather">
<span class="bi bi-list-nested-nav-menu" aria-hidden="true"></span> Weather
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="catalogs">
<span class="bi bi-collection-nav-menu" aria-hidden="true"></span> Catalogs
@@ -33,13 +22,13 @@
<div class="nav-item px-3">
<NavLink class="nav-link" href="dashboards">
<span class="oi oi-list-rich" aria-hidden="true"></span> Dashboards
<span class="bi bi-speedometer-nav-menu" aria-hidden="true"></span> Dashboards
</NavLink>
</div>
<div class="nav-item px-3">
<NavLink class="nav-link" href="massdata">
<span class="bi bi-table" aria-hidden="true"></span> MassData
<span class="bi bi-table-nav-menu" aria-hidden="true"></span> MassData
</NavLink>
</div>
</nav>

View File

@@ -46,6 +46,18 @@
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E");
}
.bi-collection-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M2 3a.5.5 0 0 1 .5-.5h11a.5.5 0 0 1 .5.5v1H2V3z'/%3E%3Cpath d='M2 5h12v8a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5z'/%3E%3C/svg%3E");
}
.bi-speedometer-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 11a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1H2a1 1 0 0 0-1 1v4zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1v8zm5 0a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1V9a1 1 0 0 0-1-1h-2a1 1 0 0 0-1 1v2z'/%3E%3C/svg%3E");
}
.bi-table-nav-menu {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' viewBox='0 0 16 16'%3E%3Cpath d='M1 2a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2zm1 1v2h12V3H2zm12 3H2v2h12V6zm0 3H2v2h12V9zm0 3H2v1h12v-1z'/%3E%3C/svg%3E");
}
.nav-item {
font-size: 0.9rem;
padding-bottom: 0.5rem;

View File

@@ -3,6 +3,7 @@
@using Microsoft.AspNetCore.Components.Rendering
@using Microsoft.AspNetCore.Components.Forms
@using DevExpress.Blazor
@using DevExpress.Data.Filtering
@inject MassDataApiClient Api
@inject LayoutApiClient LayoutApi
@inject IJSRuntime JsRuntime
@@ -96,6 +97,25 @@
.band-columns {
max-width: 720px;
}
.filter-row-cell {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: wrap;
}
.filter-operator {
width: 52px;
min-width: 52px;
flex: 0 0 52px;
}
.filter-value {
min-width: 140px;
flex: 1 1 140px;
}
.filter-value-amount {
min-width: 110px;
flex-basis: 110px;
}
</style>
@if (!string.IsNullOrWhiteSpace(errorMessage))
@@ -163,6 +183,9 @@ else
<DxGrid Data="@items"
TItem="MassDataReadDto"
KeyFieldName="@nameof(MassDataReadDto.Id)"
ShowGroupPanel="true"
ShowGroupedColumns="true"
AllowGroup="true"
ShowFilterRow="true"
AllowColumnResize="true"
ColumnResizeMode="GridColumnResizeMode.ColumnsContainer"
@@ -231,6 +254,12 @@ else
private int pageCount = 1;
private int? pageSize = 100;
private string popupHeaderText = "Edit";
private decimal? amountFilterValue;
private string amountFilterOperator = "=";
private DateTime? addedWhenFilterValue;
private string addedWhenFilterOperator = "=";
private DateTime? changedWhenFilterValue;
private string changedWhenFilterOperator = "=";
private EditContext? editContext;
private ValidationMessageStore? validationMessageStore;
private IGrid? gridRef;
@@ -277,6 +306,17 @@ else
new() { Value = 0, Text = "PRMassdata_UpsertByCustomerName" }
};
private readonly List<FilterOperatorOption> filterOperators = new()
{
new() { Value = "=", Text = "=" },
new() { Value = "<", Text = "<" },
new() { Value = ">", Text = ">" },
new() { Value = "<=", Text = "<=" },
new() { Value = ">=", Text = ">=" }
};
private bool gridLayoutApplied;
protected override async Task OnInitializedAsync()
{
columnLookup = columnDefinitions.ToDictionary(column => column.FieldName, StringComparer.OrdinalIgnoreCase);
@@ -353,7 +393,6 @@ else
columnBandAssignments = BuildAssignmentsFromLayout(bandLayout);
ApplyColumnLayoutFromStorage();
//ApplyBandOrderingFromColumnOrder();
UpdateBandOptions();
}
@@ -423,49 +462,6 @@ else
infoMessage = "Band-Layout zurückgesetzt.";
}
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()
{
foreach (var column in columnDefinitions)
@@ -642,9 +638,163 @@ else
builder.AddAttribute(seq++, "ReadOnly", true);
}
if (string.Equals(column.FieldName, nameof(MassDataReadDto.Amount), StringComparison.OrdinalIgnoreCase))
{
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildAmountFilterTemplate());
}
else if (string.Equals(column.FieldName, nameof(MassDataReadDto.AddedWhen), StringComparison.OrdinalIgnoreCase))
{
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildAddedWhenFilterTemplate());
}
else if (string.Equals(column.FieldName, nameof(MassDataReadDto.ChangedWhen), StringComparison.OrdinalIgnoreCase))
{
builder.AddAttribute(seq++, "FilterRowCellTemplate", BuildChangedWhenFilterTemplate());
}
builder.CloseComponent();
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildAmountFilterTemplate()
{
return BuildNumberFilterTemplate(
() => amountFilterValue,
value => amountFilterValue = value,
() => amountFilterOperator,
value => amountFilterOperator = value);
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildAddedWhenFilterTemplate()
{
return BuildDateFilterTemplate(
() => addedWhenFilterValue,
value => addedWhenFilterValue = value,
() => addedWhenFilterOperator,
value => addedWhenFilterOperator = value);
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildChangedWhenFilterTemplate()
{
return BuildDateFilterTemplate(
() => changedWhenFilterValue,
value => changedWhenFilterValue = value,
() => changedWhenFilterOperator,
value => changedWhenFilterOperator = value);
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildNumberFilterTemplate(
Func<decimal?> getValue,
Action<decimal?> setValue,
Func<string> getOperator,
Action<string> setOperator)
{
return context => builder =>
{
var seq = 0;
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", "filter-row-cell");
builder.OpenComponent<DxComboBox<FilterOperatorOption, string>>(seq++);
builder.AddAttribute(seq++, "Data", filterOperators);
builder.AddAttribute(seq++, "TextFieldName", "Text");
builder.AddAttribute(seq++, "ValueFieldName", "Value");
builder.AddAttribute(seq++, "Value", getOperator());
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<string>(this, value =>
{
setOperator(value);
UpdateFilterCriteria(context, value, getValue());
}));
builder.AddAttribute(seq++, "CssClass", "filter-operator");
builder.CloseComponent();
builder.OpenComponent<DxSpinEdit<decimal?>>(seq++);
builder.AddAttribute(seq++, "Value", getValue());
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<decimal?>(this, value =>
{
setValue(value);
UpdateFilterCriteria(context, getOperator(), value);
}));
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.AddAttribute(seq++, "CssClass", "filter-value filter-value-amount");
builder.CloseComponent();
builder.CloseElement();
};
}
private RenderFragment<GridDataColumnFilterRowCellTemplateContext> BuildDateFilterTemplate(
Func<DateTime?> getValue,
Action<DateTime?> setValue,
Func<string> getOperator,
Action<string> setOperator)
{
return context => builder =>
{
var seq = 0;
builder.OpenElement(seq++, "div");
builder.AddAttribute(seq++, "class", "filter-row-cell");
builder.OpenComponent<DxComboBox<FilterOperatorOption, string>>(seq++);
builder.AddAttribute(seq++, "Data", filterOperators);
builder.AddAttribute(seq++, "TextFieldName", "Text");
builder.AddAttribute(seq++, "ValueFieldName", "Value");
builder.AddAttribute(seq++, "Value", getOperator());
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<string>(this, value =>
{
setOperator(value);
UpdateFilterCriteria(context, value, getValue());
}));
builder.AddAttribute(seq++, "CssClass", "filter-operator");
builder.CloseComponent();
builder.OpenComponent<DxDateEdit<DateTime?>>(seq++);
builder.AddAttribute(seq++, "Date", getValue());
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateTime?>(this, value =>
{
setValue(value);
UpdateFilterCriteria(context, getOperator(), value);
}));
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
builder.AddAttribute(seq++, "CssClass", "filter-value");
builder.CloseComponent();
builder.CloseElement();
};
}
private void UpdateFilterCriteria(GridDataColumnFilterRowCellTemplateContext context, string op, object? value)
{
if (value == null)
{
context.FilterCriteria = null;
return;
}
var prop = new OperandProperty(context.DataColumn.FieldName);
if (value is DateTime dateValue)
{
var date = dateValue.Date;
context.FilterCriteria = op switch
{
"=" => new GroupOperator(GroupOperatorType.And, prop >= date, prop < date.AddDays(1)),
"<" => prop < date,
"<=" => prop < date.AddDays(1),
">" => prop >= date.AddDays(1),
">=" => prop >= date,
_ => prop == date
};
return;
}
context.FilterCriteria = op switch
{
"<" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Less),
">" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Greater),
"<=" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.LessOrEqual),
">=" => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.GreaterOrEqual),
_ => new BinaryOperator(prop, new OperandValue(value), BinaryOperatorType.Equal)
};
}
private void SetEditContext(EditContext context)
{
if (editContext == context)
@@ -849,7 +999,11 @@ else
public string Text { get; set; } = string.Empty;
}
private bool gridLayoutApplied;
private sealed class FilterOperatorOption
{
public string Value { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{

View File

@@ -1,19 +0,0 @@
@page "/counter"
@rendermode InteractiveServer
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
private int currentCount = 0;
private void IncrementCount()
{
currentCount++;
}
}

View File

@@ -1,64 +0,0 @@
@page "/weather"
@attribute [StreamRendering]
<PageTitle>Weather</PageTitle>
<h1>Weather</h1>
<p>This component demonstrates showing data.</p>
@if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
@foreach (var forecast in forecasts)
{
<tr>
<td>@forecast.Date.ToShortDateString()</td>
<td>@forecast.TemperatureC</td>
<td>@forecast.TemperatureF</td>
<td>@forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
@code {
private WeatherForecast[]? forecasts;
protected override async Task OnInitializedAsync()
{
// Simulate asynchronous loading to demonstrate streaming rendering
await Task.Delay(500);
var startDate = DateOnly.FromDateTime(DateTime.Now);
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = startDate.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = summaries[Random.Shared.Next(summaries.Length)]
}).ToArray();
}
private class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public string? Summary { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}
}