Refactor MgGridBase filter panel & unify filter criteria
- Moved quick-filter panel below toolbar in MgGridBase for improved layout and visual alignment; FilterPanel now renders as a styled band between toolbar and group panel. - Exposed GridSizeMode on IMgGridBase and MgGridBase; MgTagBox now inherits grid density by default for consistent sizing. - Renamed FilterText to CustomFilterCriteria across grid/data source classes and tests to clarify its role as a DevExpress criteria string. - Updated documentation (README.md, MGGRID_PARAMETERS.md, GLOSSARY.md, ADR 0004) to reflect filter panel placement, parameter naming, and sizing/layout changes. - Adjusted CSS for new filter panel layout and visual integration. - Updated example usages and tests to match new API and layout.
This commit is contained in:
parent
2d62fad300
commit
1e518d9a1e
|
|
@ -110,6 +110,13 @@ public interface IMgGridBase : IGrid
|
|||
|
||||
/// <summary>Shows/hides the quick-filter panel. No-op when the grid has no filter panel.</summary>
|
||||
void ToggleFilterPanel();
|
||||
|
||||
/// <summary>
|
||||
/// The grid's current DevExpress <c>SizeMode</c> (from <c>DxGrid</c>). Quick-filter controls in the
|
||||
/// <c>FilterPanel</c> (e.g. <see cref="MgTagBox{TData,TValue}"/>) inherit it as their default when the
|
||||
/// consumer didn't set one, so the panel matches the grid's density.
|
||||
/// </summary>
|
||||
SizeMode? GridSizeMode { get; }
|
||||
}
|
||||
|
||||
public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient> : DxGrid, IMgGridBase, IAsyncDisposable
|
||||
|
|
@ -260,13 +267,6 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
// Body
|
||||
contentBuilder.OpenElement(12, "div");
|
||||
contentBuilder.AddAttribute(13, "class", "mg-fullscreen-body");
|
||||
if (FilterPanel != null && IsFilterPanelVisible)
|
||||
{
|
||||
contentBuilder.OpenElement(14, "div");
|
||||
contentBuilder.AddAttribute(15, "class", "mg-grid-filter-panel");
|
||||
contentBuilder.AddContent(16, FilterPanel);
|
||||
contentBuilder.CloseElement();
|
||||
}
|
||||
base.BuildRenderTree(contentBuilder);
|
||||
contentBuilder.CloseElement(); // body div
|
||||
|
||||
|
|
@ -278,13 +278,6 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
contentBuilder.OpenElement(0, "div");
|
||||
contentBuilder.SetKey(_gridRenderKey);
|
||||
contentBuilder.AddAttribute(1, "style", "display: contents;");
|
||||
if (FilterPanel != null && IsFilterPanelVisible)
|
||||
{
|
||||
contentBuilder.OpenElement(2, "div");
|
||||
contentBuilder.AddAttribute(3, "class", "mg-grid-filter-panel");
|
||||
contentBuilder.AddContent(4, FilterPanel);
|
||||
contentBuilder.CloseElement();
|
||||
}
|
||||
base.BuildRenderTree(contentBuilder);
|
||||
contentBuilder.CloseElement();
|
||||
}
|
||||
|
|
@ -315,7 +308,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
[Parameter] public string Caption { get; set; } = typeof(TDataItem).Name;
|
||||
|
||||
/// <summary>
|
||||
/// Optional quick-filter controls (typically <see cref="MgTagBox{TData,TValue}"/>) rendered above the grid
|
||||
/// Optional quick-filter controls (typically <see cref="MgTagBox{TData,TValue}"/>) rendered below the toolbar (composed into the grid's ToolbarTemplate)
|
||||
/// inside a styled <c>.mg-grid-filter-panel</c> container (the slot wraps the content). Within this grid's <see cref="IMgGridBase"/>
|
||||
/// cascade, so the controls can reach this grid and apply per-field filters. See ADR 0004.
|
||||
/// </summary>
|
||||
|
|
@ -337,6 +330,56 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public SizeMode? GridSizeMode => this.SizeMode;
|
||||
|
||||
private RenderFragment<GridToolbarTemplateContext>? _consumerToolbarTemplate;
|
||||
private RenderFragment<GridToolbarTemplateContext>? _wrappedToolbarTemplate;
|
||||
|
||||
/// <summary>
|
||||
/// Renders the <see cref="FilterPanel"/> BELOW the toolbar by composing it into the grid's ToolbarTemplate:
|
||||
/// the consumer's toolbar first, then the styled <c>.mg-grid-filter-panel</c> container. Re-wraps whenever the
|
||||
/// consumer supplies a new template instance (Blazor re-applies markup parameters on every SetParametersAsync);
|
||||
/// no-op when the incoming template is already our wrapper. <see cref="IsFilterPanelVisible"/> is read at render
|
||||
/// time inside the fragment, so the toolbar toggle works without re-wrapping.
|
||||
/// COMPILE-VERIFY: ToolbarTemplate is RenderFragment<GridToolbarTemplateContext> on this DevExpress version.
|
||||
/// </summary>
|
||||
private void WrapToolbarTemplateWithFilterPanel()
|
||||
{
|
||||
if (FilterPanel == null) return;
|
||||
if (ReferenceEquals(ToolbarTemplate, _wrappedToolbarTemplate)) return;
|
||||
|
||||
_consumerToolbarTemplate = ToolbarTemplate;
|
||||
|
||||
_wrappedToolbarTemplate = context => builder =>
|
||||
{
|
||||
// Wrap toolbar + filter panel in a vertical (column) container so the panel renders BELOW the toolbar,
|
||||
// not beside it — the grid's toolbar region lays its children out horizontally otherwise.
|
||||
builder.OpenElement(0, "div");
|
||||
builder.AddAttribute(1, "class", "mg-grid-toolbar-with-filter");
|
||||
|
||||
// Toolbar row carries the toolbar-container's native padding tokens (the container's own padding is
|
||||
// zeroed by CSS when it hosts this wrapper) so the toolbar keeps its exact native look while the
|
||||
// filter band below can span the grid's full width. See .mg-grid-toolbar-row in mg-grid-info-panel.css.
|
||||
builder.OpenElement(2, "div");
|
||||
builder.AddAttribute(3, "class", "mg-grid-toolbar-row");
|
||||
if (_consumerToolbarTemplate != null) builder.AddContent(4, _consumerToolbarTemplate(context));
|
||||
builder.CloseElement();
|
||||
|
||||
if (IsFilterPanelVisible)
|
||||
{
|
||||
builder.OpenElement(5, "div");
|
||||
builder.AddAttribute(6, "class", "mg-grid-filter-panel");
|
||||
builder.AddContent(7, FilterPanel);
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
builder.CloseElement();
|
||||
};
|
||||
|
||||
ToolbarTemplate = _wrappedToolbarTemplate;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Name for auto-saving/loading grid layout. If not set, defaults to "Grid{TDataItem.Name}"
|
||||
/// </summary>
|
||||
|
|
@ -345,19 +388,25 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
public bool IsMasterGrid => ParentDataItem == null;
|
||||
protected PropertyInfo? KeyFieldPropertyInfoToParent;
|
||||
|
||||
private string? _filterText = null;
|
||||
private string? _customFilterCriteria = null;
|
||||
|
||||
/// <summary>
|
||||
/// Consumer-supplied base filter criteria (a DevExpress CriteriaOperator string) — propagated to the data source
|
||||
/// (appended as the LAST positional GetAll parameter) and triggers a reload; the server parses + whitelists it.
|
||||
/// Renamed FilterText → CustomFilterText → CustomFilterCriteria (2026-07-16) as the semantics settled to a
|
||||
/// criteria string. Under the ADR-0004 unified model it is AND-merged with the grid's automatic FilterCriteria.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? FilterText
|
||||
public string? CustomFilterCriteria
|
||||
{
|
||||
get => _filterText;
|
||||
get => _customFilterCriteria;
|
||||
set
|
||||
{
|
||||
_filterText = value;
|
||||
_customFilterCriteria = value;
|
||||
|
||||
if (_dataSource != null && _dataSource.FilterText != value)
|
||||
if (_dataSource != null && _dataSource.CustomFilterCriteria != value)
|
||||
{
|
||||
_dataSource.FilterText = value;
|
||||
_dataSource.CustomFilterCriteria = value;
|
||||
ReloadDataSourceAsync().Forget(Logger);
|
||||
}
|
||||
}
|
||||
|
|
@ -454,7 +503,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
var crudTags = new SignalRCrudTags(GetAllMessageTag, GetItemMessageTag, AddMessageTag, UpdateMessageTag, RemoveMessageTag);
|
||||
|
||||
_dataSource = (TSignalRDataSource)Activator.CreateInstance(typeof(TSignalRDataSource), SignalRClient, crudTags, Logger, ContextIds)!;
|
||||
_dataSource.FilterText = FilterText;
|
||||
_dataSource.CustomFilterCriteria = CustomFilterCriteria;
|
||||
|
||||
SetGridData(_dataSource.GetReferenceInnerList());
|
||||
|
||||
|
|
@ -772,6 +821,8 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
{
|
||||
await base.SetParametersAsyncCore(parameters);
|
||||
|
||||
WrapToolbarTemplateWithFilterPanel();
|
||||
|
||||
if (!IsFirstInitializeParameterCore)
|
||||
{
|
||||
//if (typeof(TDataItem) is IId<TId> || typeof(TDataItem) is IId<TId>)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Core grid system built on DevExpress `DxGrid`. For the full technical reference
|
|||
|
||||
## Key Files
|
||||
|
||||
- **`MgGridBase.cs`** — `MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient>`, the main abstract grid component. Extends `DxGrid` with SignalR CRUD, layout persistence, master-detail hierarchy, edit state tracking, fullscreen toggle, and a quick-filter `FilterPanel` slot (renders `MgTagBox` controls above the grid in a styled `.mg-grid-filter-panel` container; toolbar Show/Hide toggle).
|
||||
- **`MgGridBase.cs`** — `MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient>`, the main abstract grid component. Extends `DxGrid` with SignalR CRUD, layout persistence, master-detail hierarchy, edit state tracking, fullscreen toggle, and a quick-filter `FilterPanel` slot (renders `MgTagBox` controls below the toolbar in a styled `.mg-grid-filter-panel` container; toolbar Show/Hide toggle).
|
||||
- **`MgGridWithInfoPanel.razor`** — `DxSplitter` wrapper: grid (left) + InfoPanel (right), fullscreen overlay, splitter size persistence.
|
||||
- **`MgGridToolbarBase.cs`** — Extends `DxToolbar` with `Grid`, `RefreshClick`, and `ShowOnlyIcon` parameters.
|
||||
- **`MgGridToolbarTemplate.razor`** — Full toolbar template: New/Edit/Delete/Save/Cancel, row navigation, layout menu (Load/Save/Reset), column chooser, export, reload, filter-panel toggle (visible only when the grid has a `FilterPanel`), fullscreen. Extensible via `ToolbarItemsExtended`.
|
||||
|
|
|
|||
|
|
@ -41,6 +41,31 @@ public class MgTagBox<TData, TValue> : DxTagBox<TData, TValue>
|
|||
// Route the tag box's selection through our handler so a change applies the grid filter.
|
||||
// (The consumer does not bind Values; the selection is owned internally — see OnSelectionChanged.)
|
||||
base.ValuesChanged = EventCallback.Factory.Create<IEnumerable<TValue>>(this, OnSelectionChanged);
|
||||
|
||||
InheritSizeModeFromGrid();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defaults <c>SizeMode</c> to the grid's (via the <see cref="Grid"/> cascade) so the filter panel matches the
|
||||
/// grid's density — but only when the consumer left it unset (<c>null</c>); an explicit consumer value wins.
|
||||
/// Once inherited, <c>SizeMode</c> is non-null, so this defaults once rather than overriding every render.
|
||||
/// Imperative parameter writes on a DevExpress editor must be wrapped in <c>BeginUpdate</c>/<c>EndUpdate</c>
|
||||
/// (same reason as <c>Values</c> in <see cref="OnSelectionChanged"/>).
|
||||
/// </summary>
|
||||
private void InheritSizeModeFromGrid()
|
||||
{
|
||||
// SizeMode != null => the consumer set it (or we already inherited it) => leave it alone.
|
||||
if (this.SizeMode is not null || Grid?.GridSizeMode is not { } gridSizeMode) return;
|
||||
|
||||
BeginUpdate();
|
||||
try
|
||||
{
|
||||
this.SizeMode = gridSizeMode;
|
||||
}
|
||||
finally
|
||||
{
|
||||
EndUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(IEnumerable<TValue>? values)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
├── Validate Logger, SignalRClient (throw if null)
|
||||
├── Create SignalRCrudTags from message tag parameters
|
||||
├── Create TSignalRDataSource via Activator.CreateInstance(SignalRClient, crudTags, ContextIds)
|
||||
├── Set DataSource.FilterText
|
||||
├── Set DataSource.CustomFilterCriteria
|
||||
├── Bind grid Data to data source inner list
|
||||
└── Subscribe to: OnDataSourceLoaded, OnDataSourceItemChanged, OnSyncingStateChanged
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -14,7 +14,7 @@
|
|||
- **InfoPanel integration** — side panel shows focused row details, supports edit mode
|
||||
- **Fullscreen mode** — standalone overlay or via `MgGridWithInfoPanel` wrapper
|
||||
- **Change tracking** — client-side tracking with server sync via `SaveChangesAsync`
|
||||
- **Quick-filter panel** — `FilterPanel` slot above the grid hosting `MgTagBox` controls; per-field criteria AND-combined by DevExpress with the user's filter row; toolbar Show/Hide toggle (see `MGGRID_PARAMETERS.md` → Quick-Filter Panel; design: AyCode.Core repo `docs/adr/0004-signalr-datasource-server-side-filtering.md`)
|
||||
- **Quick-filter panel** — `FilterPanel` slot below the toolbar hosting `MgTagBox` controls; per-field criteria AND-combined by DevExpress with the user's filter row; toolbar Show/Hide toggle (see `MGGRID_PARAMETERS.md` → Quick-Filter Panel; design: AyCode.Core repo `docs/adr/0004-signalr-datasource-server-side-filtering.md`)
|
||||
|
||||
## Detailed Documentation
|
||||
|
||||
|
|
@ -101,6 +101,7 @@ The public contract exposed to companion components (toolbar, InfoPanel, wrapper
|
|||
| `HasFilterPanel` | `bool` | Whether the grid has a `FilterPanel` (drives toolbar-toggle visibility) |
|
||||
| `IsFilterPanelVisible` | `bool` | Current quick-filter panel visibility |
|
||||
| `ToggleFilterPanel()` | `void` | Show/hide the quick-filter panel |
|
||||
| `GridSizeMode` | `SizeMode?` | The grid's DevExpress `SizeMode`; `MgTagBox` in the `FilterPanel` inherits it as its default when the consumer sets none |
|
||||
| `SaveUserLayoutAsync()` | `Task` | Save layout manually |
|
||||
| `LoadUserLayoutAsync()` | `Task` | Load manually saved layout |
|
||||
| `ResetLayoutAsync()` | `Task` | Reset to default layout |
|
||||
|
|
|
|||
|
|
@ -5,16 +5,51 @@
|
|||
These are defined on .dxbl-theme-fluent class and inherited by child elements.
|
||||
*/
|
||||
|
||||
/* Grid quick-filter panel (rendered above the grid; hosts MgTagBox quick-filter controls) */
|
||||
/* Grid toolbar + quick-filter column: stacks the filter panel BELOW the toolbar
|
||||
(the grid's toolbar region lays its children out horizontally otherwise) */
|
||||
.mg-grid-toolbar-with-filter {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Grid quick-filter panel — composed into the grid's ToolbarTemplate, so it physically lives INSIDE
|
||||
.dxbl-grid-toolbar-container. Styled to read as its OWN band below the toolbar, aligned with the native
|
||||
group panel ("Drag a column header...") band: it reuses the grid's OWN group-panel padding tokens, which
|
||||
the grid root redefines per SizeMode — so the band's left edge and height track the group panel exactly
|
||||
at every grid size. Separator above = our border-top (grid border tokens); separator below = the toolbar
|
||||
container's own native border-bottom (its padding is zeroed below, so that border sits directly under
|
||||
this band — one line, no double border, no extra gap). */
|
||||
.mg-grid-filter-panel {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--DS-sizing-80, 0.5rem);
|
||||
padding: var(--DS-sizing-80, 0.5rem) var(--DS-sizing-160, 1rem);
|
||||
background-color: var(--DS-color-surface-neutral-subdued-rest, #f8f9fa);
|
||||
border: 1px solid var(--DS-color-border-neutral-default-rest, #dee2e6);
|
||||
border-bottom: none;
|
||||
padding: var(--dxbl-grid-group-panel-container-padding-y, 0.5rem) var(--dxbl-grid-group-panel-container-padding-x, 0.75rem);
|
||||
border-top: var(--dxbl-grid-border-width, 1px) var(--dxbl-grid-border-style, solid) var(--dxbl-grid-border-color, #dee2e6);
|
||||
}
|
||||
|
||||
/* The toolbar container's own padding wrapped BOTH rows (toolbar + filter band), which broke the band
|
||||
illusion: the band's separators stopped short of the grid's edges, and below the band the container's
|
||||
leftover bottom padding + its native border produced a second line and an asymmetric gap. Zero the
|
||||
container's padding when it hosts our stacked wrapper; each row then supplies its own padding. */
|
||||
.dxbl-grid-toolbar-container:has(.mg-grid-toolbar-with-filter) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Toolbar row inside the wrapper: carries the toolbar-container's native padding tokens (SizeMode-adaptive),
|
||||
so the toolbar itself looks exactly as it does without a filter panel. */
|
||||
.mg-grid-toolbar-row {
|
||||
padding: var(--dxbl-grid-toolbar-container-padding-y, 0.5rem) var(--dxbl-grid-toolbar-container-padding-x, 0.75rem);
|
||||
}
|
||||
|
||||
/* Quick-filter controls: sit side by side and share the row width; wrap to the next line only when
|
||||
they can't fit. Overrides the editors' default (near) full-width so they don't stack one-per-row.
|
||||
flex-basis / min / max are tunable if a different density is wanted. */
|
||||
.mg-grid-filter-panel > * {
|
||||
flex: 1 1 240px;
|
||||
min-width: 200px;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
/* Main panel - uses DevExpress Design System variables */
|
||||
|
|
|
|||
|
|
@ -17,14 +17,16 @@ For full technical reference see `AyCode.Blazor.Components/docs/MGGRID/README.md
|
|||
|
||||
| Term | Definition |
|
||||
|---|---|
|
||||
| **MgGridBase** | Abstract generic grid component extending `DxGrid` with SignalR CRUD, layout persistence, master-detail, InfoPanel, fullscreen. |
|
||||
| **MgGridBase** | Abstract generic grid component extending `DxGrid` with SignalR CRUD, layout persistence, master-detail, InfoPanel, fullscreen, and a quick-filter `FilterPanel` slot. |
|
||||
| **FilterPanel** | `MgGridBase` `RenderFragment` slot for quick-filter controls (`MgTagBox`); rendered below the toolbar as its own full-bleed band (styled like the native group panel) in a `.mg-grid-filter-panel` container, with a toolbar Show/Hide toggle. |
|
||||
| **MgTagBox** | `DxTagBox` seam with a `FieldName` — a selection applies a per-field `IN` criteria to the grid via `IMgGridBase.SetFieldQuickFilter` (DevExpress `SetFieldFilterCriteria`); used inside `FilterPanel`. Defaults its `SizeMode` to the grid's when unset; bind to lookup objects (`ValueFieldName`/`TextFieldName`) to filter an FK column by name. |
|
||||
| **MgGridWithInfoPanel** | `DxSplitter` wrapper: grid (left pane) + InfoPanel (right pane), fullscreen overlay, splitter size persistence. |
|
||||
| **MgGridToolbarBase** | `DxToolbar` base with `Grid` (`IMgGridBase`) reference and `RefreshClick` callback. |
|
||||
| **MgGridToolbarTemplate** | Full toolbar: New/Edit/Delete/Save/Cancel, row navigation, layout menu, export, fullscreen. Extensible via `ToolbarItemsExtended`. |
|
||||
| **MgGridToolbarTemplate** | Full toolbar: New/Edit/Delete/Save/Cancel, row navigation, layout menu, export, filter-panel toggle (when the grid has a `FilterPanel`), fullscreen. Extensible via `ToolbarItemsExtended`. |
|
||||
| **MgGridDataColumn** | Extended `DxGridDataColumn` with InfoPanel parameters and `UrlLink` template (`{Property}` placeholders). |
|
||||
| **MgGridInfoPanel** | Default InfoPanel: column-value pairs for focused row, responsive columns, edit mode with typed editors, template system. |
|
||||
| **MgGridSignalRDataSource** | `GridCustomDataSource` wrapping `AcSignalRDataSource`. Local cache, background refresh. |
|
||||
| **IMgGridBase** | Public interface: `IsSyncing`, `GridEditState`, `ParentGrid`, `StepPrevRow/NextRow`, layout persistence methods. |
|
||||
| **IMgGridBase** | Public interface: `IsSyncing`, `GridEditState`, `ParentGrid`, `StepPrevRow/NextRow`, layout persistence, quick-filter (`SetFieldQuickFilter`, `HasFilterPanel`, `IsFilterPanelVisible`, `ToggleFilterPanel`), `GridSizeMode`. |
|
||||
| **MgGridEditState** | Enum: `None` (no edit), `New` (adding item), `Edit` (modifying item). |
|
||||
| **SignalRCrudTags** | Bundle of 5 integer message tags (GetAll, GetItem, Add, Update, Remove) for one entity type. See `AyCode.Services/docs/SIGNALR_DATASOURCE/README.md` in AyCode.Core repo. |
|
||||
| **IsMasterGrid** | `true` when `ParentDataItem == null` — top-level grid (not detail). |
|
||||
|
|
|
|||
Loading…
Reference in New Issue