Compare commits

...

2 Commits

Author SHA1 Message Date
Loretta 4eb0d1566c Add quick-filter panel support to MgGridBase
Introduced a FilterPanel slot and supporting components for per-field quick filtering in DevExpress Blazor grids. Added MgGridFilterPanelBase and MgTagBox for declarative, AND-combined field filters. Updated IMgGridBase and base classes to support SetFieldQuickFilter. Documented the extension in ADR 0004 and clarified SearchText handling. Registered DevExpress docs fetch domain and performed minor cleanups.
2026-07-15 15:20:44 +02:00
Loretta 27d5e03955 Centralize environment state in AcEnv framework
Refactored FruitBankConst and FruitBankConstClient to delegate PROD/DEV environment logic to AcEnv.IsProdDb, removing local state and events. Updated MGGRID_TODO.md with ADR reference. Added ADR 0002 documenting the migration of environment infrastructure (BaseUrl, env state, badge) to the framework. Updated settings.local.json with new doc/code read commands and improved build error reporting.
2026-07-15 10:16:26 +02:00
4 changed files with 132 additions and 0 deletions

View File

@ -6,6 +6,7 @@ using AyCode.Core.Loggers;
using AyCode.Services.SignalRs;
using AyCode.Utils.Extensions;
using DevExpress.Blazor;
using DevExpress.Data.Filtering;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.JSInterop;
@ -93,6 +94,13 @@ public interface IMgGridBase : IGrid
/// returns true optimistically when server layouts are enabled (no server round-trip)
/// </summary>
Task<bool> HasUserLayoutAsync();
/// <summary>
/// Applies a per-field quick filter to the grid — delegates to DevExpress <c>SetFieldFilterCriteria</c>,
/// which AND-combines it with the user's filter row (and other fields' quick filters). Passing
/// <paramref name="criteria"/> = null clears this field's filter. See ADR 0004.
/// </summary>
void SetFieldQuickFilter(string fieldName, CriteriaOperator? criteria);
}
public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient> : DxGrid, IMgGridBase, IAsyncDisposable
@ -243,6 +251,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
// Body
contentBuilder.OpenElement(12, "div");
contentBuilder.AddAttribute(13, "class", "mg-fullscreen-body");
if (FilterPanel != null) contentBuilder.AddContent(14, FilterPanel);
base.BuildRenderTree(contentBuilder);
contentBuilder.CloseElement(); // body div
@ -254,6 +263,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
contentBuilder.OpenElement(0, "div");
contentBuilder.SetKey(_gridRenderKey);
contentBuilder.AddAttribute(1, "style", "display: contents;");
if (FilterPanel != null) contentBuilder.AddContent(2, FilterPanel);
base.BuildRenderTree(contentBuilder);
contentBuilder.CloseElement();
}
@ -283,6 +293,13 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
[Parameter] public string Caption { get; set; } = typeof(TDataItem).Name;
/// <summary>
/// Optional quick-filter panel rendered above the grid (typically a <c>GridFilterPanel</c> hosting
/// <see cref="MgTagBox{TData,TValue}"/> controls). Rendered within this grid's <see cref="IMgGridBase"/>
/// cascade, so the controls can reach this grid and apply per-field filters. See ADR 0004.
/// </summary>
[Parameter] public RenderFragment? FilterPanel { get; set; }
/// <summary>
/// Name for auto-saving/loading grid layout. If not set, defaults to "Grid{TDataItem.Name}"
/// </summary>
@ -1004,6 +1021,10 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
return _dataSource.LoadDataSourceAsync(false);
}
/// <inheritdoc />
public void SetFieldQuickFilter(string fieldName, CriteriaOperator? criteria)
=> SetFieldFilterCriteria(fieldName, criteria);
/// <summary>
/// Navigates to the previous row in the grid
/// </summary>

View File

@ -0,0 +1,32 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
namespace AyCode.Blazor.Components.Components.Grids;
/// <summary>
/// Base for a grid quick-filter panel — a styled container placed in a grid's <c>FilterPanel</c> slot,
/// hosting <see cref="MgTagBox{TData,TValue}"/> (or other) per-field quick-filter controls above the grid.
/// <para>
/// Framework base. Projects derive a (typically empty) <c>GridFilterPanel</c> per the Mg* seam policy
/// (cf. <c>MgGridBase</c> → project grid). The grid reference reaches the child controls via the grid's own
/// <see cref="IMgGridBase"/> cascade (<c>MgGridBase</c> provides it) — this panel is layout only, no wiring.
/// </para>
/// </summary>
public class MgGridFilterPanelBase : ComponentBase
{
/// <summary>The quick-filter controls (e.g. <see cref="MgTagBox{TData,TValue}"/>) to render in the panel.</summary>
[Parameter] public RenderFragment? ChildContent { get; set; }
/// <summary>Extra CSS classes appended to the panel container.</summary>
[Parameter] public string? CssClass { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenElement(0, "div");
builder.AddAttribute(1, "class", string.IsNullOrWhiteSpace(CssClass)
? "mg-grid-filter-panel"
: $"mg-grid-filter-panel {CssClass}");
builder.AddContent(2, ChildContent);
builder.CloseElement();
}
}

View File

@ -0,0 +1,77 @@
using AyCode.Blazor.Components.Components.Grids;
using DevExpress.Blazor;
using DevExpress.Data.Filtering;
using Microsoft.AspNetCore.Components;
namespace AyCode.Blazor.Components.Components;
/// <summary>
/// <c>DxTagBox</c> seam (per the Mg* "derive every DevExpress component" policy) with an added grid
/// quick-filter capability.
/// <para>
/// When placed inside a grid's <c>FilterPanel</c>, changing the selection applies a per-field
/// <c>IN(FieldName, selected...)</c> filter to the grid via <see cref="IMgGridBase.SetFieldQuickFilter"/>.
/// DevExpress AND-combines it with the user's filter row (it uses <c>SetFieldFilterCriteria</c>, NOT
/// <c>SetFilterCriteria</c>) — see ADR 0004.
/// </para>
/// The selection is managed internally, so the consumer writes just
/// <c>&lt;MgTagBox FieldName="..." Data="..." /&gt;</c> — no <c>@bind-Values</c> / backing field needed.
/// Outside a grid filter panel (no <see cref="Grid"/> cascade) it behaves as a plain tag box.
/// </summary>
/// <typeparam name="TData">Data item type (as for <c>DxTagBox</c>).</typeparam>
/// <typeparam name="TValue">Selected value type (as for <c>DxTagBox</c>).</typeparam>
public class MgTagBox<TData, TValue> : DxTagBox<TData, TValue>
{
/// <summary>
/// Grid column field this quick-filter targets. Consistent with <c>DxGridDataColumn.FieldName</c> and the
/// <c>SetFieldFilterCriteria(fieldName, ...)</c> parameter. When null/empty, no grid filter is applied.
/// </summary>
[Parameter] public string? FieldName { get; set; }
/// <summary>
/// The grid to filter — supplied by the grid's <c>FilterPanel</c> cascade (<c>MgGridBase</c> provides the
/// <see cref="IMgGridBase"/> cascade). Null when the tag box is not inside a grid filter panel.
/// </summary>
[CascadingParameter] public IMgGridBase? Grid { get; set; }
private bool _initialized;
private IEnumerable<TValue>? _selected;
protected override void OnParametersSet()
{
base.OnParametersSet();
// Own the selection internally so the consumer needs no @bind-Values.
// COMPILE-VERIFY (DevExpress version specific): Blazor re-applies base.Values / base.ValuesChanged from the
// consumer markup on every parameter set, so we re-assert ours here each time. First set adopts any initial Values.
if (!_initialized)
{
_selected = Values;
_initialized = true;
}
base.Values = _selected;
base.ValuesChanged = EventCallback.Factory.Create<IEnumerable<TValue>>(this, OnSelectionChanged);
}
private void OnSelectionChanged(IEnumerable<TValue>? values)
{
_selected = values;
base.Values = values; // reflect the new selection in the editor
ApplyQuickFilter(values);
StateHasChanged();
}
private void ApplyQuickFilter(IEnumerable<TValue>? values)
{
if (Grid is null || string.IsNullOrWhiteSpace(FieldName)) return;
var selected = values?.Where(v => v is not null).Cast<object>().ToArray() ?? [];
// COMPILE-VERIFY: confirm the InOperator(string, IEnumerable / params object[]) overload on your
// DevExpress.Data version. Empty selection => null => clears this field's filter.
CriteriaOperator? criteria = selected.Length > 0 ? new InOperator(FieldName, selected) : null;
Grid.SetFieldQuickFilter(FieldName, criteria);
}
}

View File

@ -2,6 +2,8 @@
For known issues / bugs see `MGGRID_ISSUES.md`.
> **Server-side grid filtering — design in ADR 0004.** MgGrid `FilterMode` (Local/Server), the criteria-string forwarding, and the `DxTagBox` quick-filter template extension are captured in `AyCode.Core/docs/adr/0004-signalr-datasource-server-side-filtering.md` (in AyCode.Core repo, Status: Proposed). Not yet broken into per-step entries — reference the ADR when implementation begins.
## Priority legend
- **P0** blocker · **P1** important · **P2** nice-to-have · **P3** idea