Compare commits
11 Commits
920bc299aa
...
15776ca537
| Author | SHA1 | Date |
|---|---|---|
|
|
15776ca537 | |
|
|
017eb16c4b | |
|
|
4c86914884 | |
|
|
5255917210 | |
|
|
739d0fa808 | |
|
|
112d633590 | |
|
|
fe1a59a0bd | |
|
|
90f12a160e | |
|
|
109a4b82b4 | |
|
|
45294199cf | |
|
|
c1cf30b8f0 |
|
|
@ -0,0 +1,22 @@
|
|||
namespace AyCode.Blazor.Components.Components.Grids;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the current edit state of the MgGrid
|
||||
/// </summary>
|
||||
public enum MgGridEditState
|
||||
{
|
||||
/// <summary>
|
||||
/// No edit operation in progress
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Adding a new row
|
||||
/// </summary>
|
||||
New,
|
||||
|
||||
/// <summary>
|
||||
/// Editing an existing row
|
||||
/// </summary>
|
||||
Edit
|
||||
}
|
||||
|
|
@ -8,8 +8,10 @@ using AyCode.Services.SignalRs;
|
|||
using AyCode.Utils.Extensions;
|
||||
using DevExpress.Blazor;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Rendering;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using DevExpress.Blazor.Internal;
|
||||
|
||||
namespace AyCode.Blazor.Components.Components.Grids;
|
||||
|
||||
|
|
@ -19,11 +21,48 @@ public interface IMgGridBase : IGrid
|
|||
/// Indicates whether any synchronization operation is in progress
|
||||
/// </summary>
|
||||
bool IsSyncing { get; }
|
||||
|
||||
string Caption { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when synchronization state changes (true = syncing started, false = syncing ended)
|
||||
/// Current edit state of the grid (None, New, Edit)
|
||||
/// </summary>
|
||||
event Action<bool>? OnSyncingStateChanged;
|
||||
MgGridEditState GridEditState { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Parent grid in nested grid hierarchy (null if this is a root grid)
|
||||
/// </summary>
|
||||
IMgGridBase? ParentGrid { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the root grid in the hierarchy
|
||||
/// </summary>
|
||||
IMgGridBase GetRootGrid();
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the previous row in the grid
|
||||
/// </summary>
|
||||
void StepPrevRow();
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the next row in the grid
|
||||
/// </summary>
|
||||
void StepNextRow();
|
||||
|
||||
/// <summary>
|
||||
/// InfoPanel instance for displaying row details (from wrapper)
|
||||
/// </summary>
|
||||
IInfoPanelBase? InfoPanelInstance { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the grid/wrapper is currently in fullscreen mode
|
||||
/// </summary>
|
||||
bool IsFullscreen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Toggles fullscreen mode for the grid (or wrapper if available)
|
||||
/// </summary>
|
||||
void ToggleFullscreen();
|
||||
}
|
||||
|
||||
public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient> : DxGrid, IMgGridBase, IAsyncDisposable
|
||||
|
|
@ -45,14 +84,120 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
|
||||
/// <inheritdoc />
|
||||
public bool IsSyncing => _dataSource?.IsSyncing ?? false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public MgGridEditState GridEditState { get; private set; } = MgGridEditState.None;
|
||||
|
||||
/// <inheritdoc />
|
||||
[CascadingParameter]
|
||||
public IMgGridBase? ParentGrid { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IMgGridBase GetRootGrid()
|
||||
{
|
||||
var current = (IMgGridBase)this;
|
||||
while (current.ParentGrid != null)
|
||||
{
|
||||
current = current.ParentGrid;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the wrapper component for grid-InfoPanel communication
|
||||
/// </summary>
|
||||
[CascadingParameter]
|
||||
public MgGridWithInfoPanel? GridWrapper { get; set; }
|
||||
|
||||
private object _focusedDataItem;
|
||||
|
||||
/// <summary>
|
||||
/// InfoPanel instance for displaying row details (from wrapper or direct)
|
||||
/// </summary>
|
||||
public IInfoPanelBase? InfoPanelInstance
|
||||
{
|
||||
get => GridWrapper?.InfoPanelInstance;
|
||||
set { /* Set through wrapper */ }
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public event Action<bool>? OnSyncingStateChanged;
|
||||
public bool IsFullscreen => GridWrapper?.IsFullscreen ?? _isStandaloneFullscreen;
|
||||
|
||||
private bool _isStandaloneFullscreen;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ToggleFullscreen()
|
||||
{
|
||||
if (GridWrapper != null)
|
||||
{
|
||||
// Ha van wrapper, azt váltjuk fullscreen-be
|
||||
GridWrapper.ToggleFullscreen();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ha nincs wrapper, saját fullscreen állapotot használunk
|
||||
_isStandaloneFullscreen = !_isStandaloneFullscreen;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
public MgGridBase() : base()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void BuildRenderTree(RenderTreeBuilder builder)
|
||||
{
|
||||
var seq = 0;
|
||||
|
||||
// Wrap everything in a CascadingValue to provide this grid as ParentGrid to nested grids
|
||||
builder.OpenComponent<CascadingValue<IMgGridBase>>(seq++);
|
||||
builder.AddAttribute(seq++, "Value", (IMgGridBase)this);
|
||||
builder.AddAttribute(seq++, "ChildContent", (RenderFragment)(contentBuilder =>
|
||||
{
|
||||
if (_isStandaloneFullscreen && GridWrapper == null)
|
||||
{
|
||||
// Standalone fullscreen mode - Bootstrap 5 fullscreen overlay
|
||||
contentBuilder.OpenElement(0, "div");
|
||||
contentBuilder.AddAttribute(1, "class", "mg-fullscreen-overlay");
|
||||
|
||||
// Header
|
||||
contentBuilder.OpenElement(2, "div");
|
||||
contentBuilder.AddAttribute(3, "class", "mg-fullscreen-header");
|
||||
|
||||
contentBuilder.OpenElement(4, "span");
|
||||
contentBuilder.AddAttribute(5, "class", "mg-fullscreen-title");
|
||||
contentBuilder.AddContent(6, Caption);
|
||||
contentBuilder.CloseElement(); // span
|
||||
|
||||
contentBuilder.OpenElement(7, "button");
|
||||
contentBuilder.AddAttribute(8, "type", "button");
|
||||
contentBuilder.AddAttribute(9, "class", "btn-close btn-close-white");
|
||||
contentBuilder.AddAttribute(10, "aria-label", "Close");
|
||||
contentBuilder.AddAttribute(11, "onclick", EventCallback.Factory.Create<Microsoft.AspNetCore.Components.Web.MouseEventArgs>(this, () =>
|
||||
{
|
||||
_isStandaloneFullscreen = false;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}));
|
||||
contentBuilder.CloseElement(); // button
|
||||
|
||||
contentBuilder.CloseElement(); // header div
|
||||
|
||||
// Body
|
||||
contentBuilder.OpenElement(12, "div");
|
||||
contentBuilder.AddAttribute(13, "class", "mg-fullscreen-body");
|
||||
base.BuildRenderTree(contentBuilder);
|
||||
contentBuilder.CloseElement(); // body div
|
||||
|
||||
contentBuilder.CloseElement(); // overlay div
|
||||
}
|
||||
else
|
||||
{
|
||||
base.BuildRenderTree(contentBuilder);
|
||||
}
|
||||
}));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
protected bool HasIdValue(TDataItem dataItem) => HasIdValue(dataItem.Id);
|
||||
protected bool HasIdValue(TId id) => !_equalityComparerId.Equals(id, default);
|
||||
protected bool IdEquals(TId id1, TId id2) => _equalityComparerId.Equals(id1, id2);
|
||||
|
|
@ -63,6 +208,8 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
[Parameter] public string? KeyFieldNameToParentId { get; set; }
|
||||
[Parameter] public object[]? ContextIds { get; set; }
|
||||
|
||||
[Parameter] public string Caption { get; set; } = typeof(TDataItem).Name;
|
||||
|
||||
public bool IsMasterGrid => ParentDataItem == null;
|
||||
protected PropertyInfo? KeyFieldPropertyInfoToParent;
|
||||
|
||||
|
|
@ -104,6 +251,9 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
protected new EventCallback<GridCustomizeEditModelEventArgs> CustomizeEditModel { get; set; }
|
||||
[Parameter] public EventCallback<GridCustomizeEditModelEventArgs> OnGridCustomizeEditModel { get; set; }
|
||||
|
||||
protected new EventCallback<GridFocusedRowChangedEventArgs> FocusedRowChanged { get; set; }
|
||||
[Parameter] public EventCallback<GridFocusedRowChangedEventArgs> OnGridFocusedRowChanged { get; set; }
|
||||
|
||||
[Parameter] public EventCallback<IList<TDataItem>> OnDataSourceChanged { get; set; }
|
||||
[Parameter] public EventCallback<GridDataItemChangingEventArgs<TDataItem>> OnGridItemChanging { get; set; }
|
||||
|
||||
|
|
@ -171,7 +321,6 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
|
||||
_dataSource = (TSignalRDataSource)Activator.CreateInstance(typeof(TSignalRDataSource), SignalRClient, crudTags, ContextIds)!;
|
||||
_dataSource.FilterText = FilterText;
|
||||
//_dataSource = new SignalRDataSource<TDataItem>(SignalRClient, crudTags, ContextIds) { FilterText = FilterText };
|
||||
|
||||
SetGridData(_dataSource.GetReferenceInnerList());
|
||||
|
||||
|
|
@ -187,7 +336,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
if (_isDisposed) return;
|
||||
|
||||
// Forward the event to external subscribers
|
||||
OnSyncingStateChanged?.Invoke(isSyncing);
|
||||
//OnSyncingStateChanged?.Invoke(isSyncing);
|
||||
|
||||
// Trigger UI update
|
||||
InvokeAsync(StateHasChanged);
|
||||
|
|
@ -217,12 +366,8 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
|
||||
Logger.Debug($"{_gridLogName} OnDataSourceLoaded; Count: {_dataSource?.Count}");
|
||||
|
||||
//if(_dataSourceParam.GetType() == typeof()AcObservableCollection<TDataItem>)
|
||||
|
||||
SetGridData(_dataSource!.GetReferenceInnerList());
|
||||
//else Reload();
|
||||
|
||||
//_dataSource.LoadItem(_dataSource.First().Id).Forget();
|
||||
if (!_isDisposed)
|
||||
{
|
||||
await OnDataSourceChanged.InvokeAsync(_dataSource);
|
||||
|
|
@ -305,29 +450,44 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
e.EditModel = editModel;
|
||||
}
|
||||
|
||||
// Set edit state
|
||||
GridEditState = e.IsNew ? MgGridEditState.New : MgGridEditState.Edit;
|
||||
|
||||
await OnGridCustomizeEditModel.InvokeAsync(e);
|
||||
|
||||
// Update InfoPanel to edit mode
|
||||
InfoPanelInstance?.SetEditMode(editModel);
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
|
||||
private async Task OnEditStart(GridEditStartEventArgs e)
|
||||
{
|
||||
var dataItem = (e.DataItem as TDataItem)!;
|
||||
|
||||
await OnGridEditStart.InvokeAsync(e);
|
||||
}
|
||||
|
||||
//void Grid_CustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
||||
//{
|
||||
// var model = e.EditModel as EditableWorkOrder;
|
||||
// if (model == null)
|
||||
// {
|
||||
// model = new EditableWorkOrder();
|
||||
protected virtual async Task OnFocusedRowChanged(GridFocusedRowChangedEventArgs e)
|
||||
{
|
||||
_focusedDataItem = e.DataItem;
|
||||
|
||||
// model.WorkOrderNum = "123";
|
||||
// model.Description = "hey";
|
||||
var infoPanelInstance = InfoPanelInstance;
|
||||
|
||||
if (infoPanelInstance != null && e.DataItem != null)
|
||||
{
|
||||
// Ha edit módban vagyunk, de a felhasználó egy másik sorra kattintott,
|
||||
// akkor kilépünk az edit módból
|
||||
if (GridEditState != MgGridEditState.None)
|
||||
{
|
||||
infoPanelInstance.ClearEditMode();
|
||||
}
|
||||
|
||||
// Frissítjük az InfoPanel-t az új sor adataival
|
||||
infoPanelInstance.RefreshData(this, e.DataItem, e.VisibleIndex);
|
||||
}
|
||||
|
||||
await OnGridFocusedRowChanged.InvokeAsync(e);
|
||||
}
|
||||
|
||||
// e.EditModel = model;
|
||||
// }
|
||||
//}
|
||||
private async Task OnItemSaving(GridEditModelSavingEventArgs e)
|
||||
{
|
||||
var dataItem = (e.EditModel as TDataItem)!;
|
||||
|
|
@ -335,15 +495,6 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
if (e.IsNew)
|
||||
{
|
||||
if (!HasIdValue(dataItem)) SetNewId(dataItem);
|
||||
|
||||
//if (ParentDataItem != null && !KeyFieldNameToParentId.IsNullOrWhiteSpace())
|
||||
//{
|
||||
// Type examType = typeof(TDataItem);
|
||||
|
||||
// // Change the static property value.
|
||||
// PropertyInfo piShared = examType.GetProperty(KeyFieldNameToParentId);
|
||||
// piShared.SetValue(dataItem, ParentDataItem.Id);
|
||||
//}
|
||||
}
|
||||
|
||||
var logText = e.IsNew ? "add" : "update";
|
||||
|
|
@ -364,11 +515,20 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
}
|
||||
else await UpdateDataItemAsync(dataItem);
|
||||
|
||||
//var equalityComparer = EqualityComparer<TId>.Default;
|
||||
//var index = CollectionExtensions.FindIndex(_dataSource, x => equalityComparer.Equals(x.Id, dataItem.Id));
|
||||
//_dataSource.UpdateCollectionByIndex(index, dataItem, false);
|
||||
GridEditState = MgGridEditState.None;
|
||||
|
||||
//_dataSource.UpdateCollectionById<TId>(dataItem.Id, false);
|
||||
InfoPanelInstance?.ClearEditMode();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private async Task OnEditCanceling(GridEditCancelingEventArgs e)
|
||||
{
|
||||
GridEditState = MgGridEditState.None;
|
||||
|
||||
InfoPanelInstance?.ClearEditMode();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private Task SaveChangesToServerAsync()
|
||||
|
|
@ -431,6 +591,27 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
e.Column.Visible = AcDomain.IsDeveloperVersion;
|
||||
e.Column.ShowInColumnChooser = AcDomain.IsDeveloperVersion;
|
||||
}
|
||||
|
||||
// Apply edit mode background to the row being edited
|
||||
if (e.ElementType == GridElementType.DataRow && GridEditState != MgGridEditState.None)
|
||||
{
|
||||
if (e.VisibleIndex == GetFocusedRowIndex())
|
||||
{
|
||||
e.Style = string.IsNullOrEmpty(e.Style)
|
||||
? "background-color: #fffbeb;"
|
||||
: e.Style + " background-color: #fffbeb;";
|
||||
}
|
||||
}
|
||||
// Apply edit mode background to cells in the edited row
|
||||
else if (e.ElementType == GridElementType.DataCell && GridEditState != MgGridEditState.None)
|
||||
{
|
||||
if (e.VisibleIndex == GetFocusedRowIndex())
|
||||
{
|
||||
e.Style = string.IsNullOrEmpty(e.Style)
|
||||
? "background-color: #fffbeb;"
|
||||
: e.Style + " background-color: #fffbeb;";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task SetParametersAsyncCore(ParameterView parameters)
|
||||
|
|
@ -445,8 +626,9 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
base.DataItemDeleting = EventCallback.Factory.Create<GridDataItemDeletingEventArgs>(this, OnItemDeleting);
|
||||
base.EditModelSaving = EventCallback.Factory.Create<GridEditModelSavingEventArgs>(this, OnItemSaving);
|
||||
base.CustomizeEditModel = EventCallback.Factory.Create<GridCustomizeEditModelEventArgs>(this, OnCustomizeEditModel);
|
||||
//base.customizecel= EventCallback.Factory.Create<GridCustomizeEditModelEventArgs>(this, OnCustomizeEditModel);
|
||||
base.FocusedRowChanged = EventCallback.Factory.Create<GridFocusedRowChangedEventArgs>(this, OnFocusedRowChanged);
|
||||
base.EditStart = EventCallback.Factory.Create<GridEditStartEventArgs>(this, OnEditStart);
|
||||
base.EditCanceling = EventCallback.Factory.Create<GridEditCancelingEventArgs>(this, OnEditCanceling);
|
||||
|
||||
CustomizeElement += OnCustomizeElement;
|
||||
|
||||
|
|
@ -460,16 +642,6 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
HighlightRowOnHover = true;
|
||||
AutoCollapseDetailRow = true;
|
||||
AutoExpandAllGroupRows = false;
|
||||
//KeyboardNavigationEnabled = true;
|
||||
|
||||
//var dataColumns = GetDataColumns();
|
||||
|
||||
//var idColumn = dataColumns.FirstOrDefault(x => x.FieldName == nameof(IId<TId>.Id));
|
||||
//if (idColumn != null)
|
||||
//{
|
||||
// idColumn.ShowInColumnChooser = AcDomain.IsDeveloperVersion;
|
||||
// idColumn.Visible = !AcDomain.IsDeveloperVersion;
|
||||
//}
|
||||
|
||||
IsFirstInitializeParameterCore = true;
|
||||
}
|
||||
|
|
@ -517,6 +689,31 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
|||
return _dataSource.LoadDataSourceAsync(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the previous row in the grid
|
||||
/// </summary>
|
||||
public void StepPrevRow()
|
||||
{
|
||||
var currentIndex = GetFocusedRowIndex();
|
||||
if (currentIndex > 0)
|
||||
{
|
||||
SetFocusedRowIndex(currentIndex - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Navigates to the next row in the grid
|
||||
/// </summary>
|
||||
public void StepNextRow()
|
||||
{
|
||||
var currentIndex = GetFocusedRowIndex();
|
||||
var visibleRowCount = GetVisibleRowCount();
|
||||
if (currentIndex >= 0 && currentIndex < visibleRowCount - 1)
|
||||
{
|
||||
SetFocusedRowIndex(currentIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_isDisposed) return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
using DevExpress.Blazor;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AyCode.Blazor.Components.Components.Grids;
|
||||
|
||||
/// <summary>
|
||||
/// Extended DxGridDataColumn with additional parameters for InfoPanel support.
|
||||
/// </summary>
|
||||
public class MgGridDataColumn : DxGridDataColumn
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this column should be visible in the InfoPanel. Default is true.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ShowInInfoPanel { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Custom display format for InfoPanel (overrides DisplayFormat if set).
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string? InfoPanelDisplayFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Column order in InfoPanel (lower = earlier). Default is int.MaxValue.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public int InfoPanelOrder { get; set; } = int.MaxValue;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace AyCode.Blazor.Components.Components.Grids
|
||||
{
|
||||
internal class MgGridHelper
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,383 @@
|
|||
@using DevExpress.Blazor
|
||||
@using Microsoft.AspNetCore.Components.Rendering
|
||||
@using System.Reflection
|
||||
|
||||
<div @ref="_panelElement" class="mg-grid-info-panel @(_isEditMode ? "edit-mode" : "") @GetColumnCountClass()">
|
||||
@* Header *@
|
||||
@if (HeaderTemplate != null)
|
||||
{
|
||||
@HeaderTemplate(GetActiveDataItem())
|
||||
}
|
||||
else if (_currentGrid != null)
|
||||
{
|
||||
<div class="mg-info-panel-header">@_currentGrid.Caption</div>
|
||||
}
|
||||
|
||||
@* Toolbar *@
|
||||
@if (_currentGrid != null)
|
||||
{
|
||||
<div class="mg-info-panel-toolbar">
|
||||
<MgGridToolbarTemplate Grid="_currentGrid" OnlyGridEditTools="true" ShowOnlyIcon="true" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Content *@
|
||||
<div class="mg-info-panel-content">
|
||||
@if (GetActiveDataItem() != null && _currentGrid != null)
|
||||
{
|
||||
@if (BeforeColumnsTemplate != null)
|
||||
{
|
||||
@BeforeColumnsTemplate(GetActiveDataItem())
|
||||
}
|
||||
|
||||
@if (ColumnsTemplate != null)
|
||||
{
|
||||
@ColumnsTemplate(GetActiveDataItem())
|
||||
}
|
||||
else
|
||||
{
|
||||
@RenderDefaultColumns()
|
||||
}
|
||||
|
||||
@if (AfterColumnsTemplate != null)
|
||||
{
|
||||
@AfterColumnsTemplate(GetActiveDataItem())
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="mg-info-panel-empty">
|
||||
<p>Válasszon ki egy sort az adatok megtekintéséhez</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@* Footer *@
|
||||
@if (FooterTemplate != null)
|
||||
{
|
||||
@FooterTemplate(GetActiveDataItem())
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public RenderFragment<object?>? HeaderTemplate { get; set; }
|
||||
[Parameter] public RenderFragment<object?>? BeforeColumnsTemplate { get; set; }
|
||||
[Parameter] public RenderFragment<object?>? ColumnsTemplate { get; set; }
|
||||
[Parameter] public RenderFragment<object?>? AfterColumnsTemplate { get; set; }
|
||||
[Parameter] public RenderFragment<object?>? FooterTemplate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Called when the data item changes (row selection changed)
|
||||
/// </summary>
|
||||
[Parameter] public EventCallback<object?> OnDataItemChanged { get; set; }
|
||||
|
||||
private string GetColumnCountClass() => FixedColumnCount switch
|
||||
{
|
||||
1 => "mg-columns-1",
|
||||
2 => "mg-columns-2",
|
||||
3 => "mg-columns-3",
|
||||
4 => "mg-columns-4",
|
||||
_ => ""
|
||||
};
|
||||
|
||||
private RenderFragment RenderDefaultColumns() => builder =>
|
||||
{
|
||||
var dataItem = GetActiveDataItem();
|
||||
if (dataItem == null) return;
|
||||
|
||||
var dataItemType = dataItem.GetType();
|
||||
var seq = 0;
|
||||
|
||||
builder.OpenElement(seq++, "div");
|
||||
builder.AddAttribute(seq++, "class", "mg-info-panel-grid");
|
||||
|
||||
foreach (var column in GetVisibleColumns())
|
||||
{
|
||||
var displayText = GetDisplayTextFromGrid(column);
|
||||
var value = GetCellValue(column);
|
||||
var settingsType = GetEditSettingsType(column);
|
||||
var isEditable = _isEditMode && !column.ReadOnly;
|
||||
|
||||
builder.OpenElement(seq++, "div");
|
||||
builder.AddAttribute(seq++, "class", "mg-info-panel-item");
|
||||
|
||||
builder.OpenElement(seq++, "label");
|
||||
builder.AddAttribute(seq++, "class", isEditable ? "mg-info-panel-label editable" : "mg-info-panel-label");
|
||||
builder.AddContent(seq++, GetColumnCaption(column));
|
||||
builder.CloseElement();
|
||||
|
||||
builder.OpenElement(seq++, "div");
|
||||
if (isEditable)
|
||||
{
|
||||
RenderEditableCell(column, dataItem, dataItemType, value, displayText, settingsType)(builder);
|
||||
}
|
||||
else
|
||||
{
|
||||
RenderCellContent(value, displayText)(builder);
|
||||
}
|
||||
builder.CloseElement();
|
||||
|
||||
builder.CloseElement();
|
||||
}
|
||||
|
||||
builder.CloseElement();
|
||||
};
|
||||
|
||||
private static string GetColumnCaption(DxGridDataColumn column) =>
|
||||
!string.IsNullOrWhiteSpace(column.Caption) ? column.Caption : column.FieldName;
|
||||
|
||||
private RenderFragment RenderEditableCell(DxGridDataColumn column, object dataItem, Type dataItemType, object? value, string displayText, EditSettingsType settingsType)
|
||||
{
|
||||
return builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
var propertyInfo = dataItemType.GetProperty(column.FieldName);
|
||||
|
||||
if (propertyInfo == null)
|
||||
{
|
||||
RenderCellContent(value, displayText)(builder);
|
||||
return;
|
||||
}
|
||||
|
||||
var underlyingType = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
|
||||
|
||||
if (settingsType == EditSettingsType.ComboBox && GetEditSettings(column.FieldName) is DxComboBoxSettings comboSettings)
|
||||
{
|
||||
RenderComboBoxEditor(builder, ref seq, dataItem, propertyInfo, comboSettings);
|
||||
return;
|
||||
}
|
||||
|
||||
if (underlyingType == typeof(bool)) RenderCheckBoxEditor(builder, ref seq, dataItem, propertyInfo);
|
||||
else if (underlyingType == typeof(DateTime)) RenderDateTimeEditor(builder, ref seq, dataItem, propertyInfo, column.DisplayFormat);
|
||||
else if (underlyingType == typeof(DateOnly)) RenderDateOnlyEditor(builder, ref seq, dataItem, propertyInfo, column.DisplayFormat);
|
||||
else if (underlyingType == typeof(int)) RenderSpinIntEditor(builder, ref seq, dataItem, propertyInfo);
|
||||
else if (underlyingType == typeof(decimal)) RenderSpinDecimalEditor(builder, ref seq, dataItem, propertyInfo);
|
||||
else if (underlyingType == typeof(double)) RenderSpinDoubleEditor(builder, ref seq, dataItem, propertyInfo);
|
||||
else if (settingsType == EditSettingsType.Memo) RenderMemoEditor(builder, ref seq, dataItem, propertyInfo);
|
||||
else RenderTextBoxEditor(builder, ref seq, dataItem, propertyInfo);
|
||||
};
|
||||
}
|
||||
|
||||
private void RenderCheckBoxEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
|
||||
{
|
||||
builder.OpenComponent<DxCheckBox<bool>>(seq++);
|
||||
builder.AddAttribute(seq++, "Checked", (bool)(propertyInfo.GetValue(dataItem) ?? false));
|
||||
builder.AddAttribute(seq++, "CheckedChanged", EventCallback.Factory.Create<bool>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderDateTimeEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, string? displayFormat)
|
||||
{
|
||||
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
|
||||
var value = propertyInfo.GetValue(dataItem);
|
||||
|
||||
if (isNullable)
|
||||
{
|
||||
builder.OpenComponent<DxDateEdit<DateTime?>>(seq++);
|
||||
builder.AddAttribute(seq++, "Date", (DateTime?)value);
|
||||
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateTime?>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.OpenComponent<DxDateEdit<DateTime>>(seq++);
|
||||
builder.AddAttribute(seq++, "Date", (DateTime)(value ?? DateTime.MinValue));
|
||||
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateTime>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
}
|
||||
builder.AddAttribute(seq++, "DisplayFormat", displayFormat ?? "yyyy-MM-dd HH:mm");
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderDateOnlyEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, string? displayFormat)
|
||||
{
|
||||
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
|
||||
var value = propertyInfo.GetValue(dataItem);
|
||||
|
||||
if (isNullable)
|
||||
{
|
||||
builder.OpenComponent<DxDateEdit<DateOnly?>>(seq++);
|
||||
builder.AddAttribute(seq++, "Date", (DateOnly?)value);
|
||||
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateOnly?>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.OpenComponent<DxDateEdit<DateOnly>>(seq++);
|
||||
builder.AddAttribute(seq++, "Date", (DateOnly)(value ?? DateOnly.MinValue));
|
||||
builder.AddAttribute(seq++, "DateChanged", EventCallback.Factory.Create<DateOnly>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
}
|
||||
builder.AddAttribute(seq++, "DisplayFormat", displayFormat ?? "yyyy-MM-dd");
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderSpinIntEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
|
||||
{
|
||||
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
|
||||
var value = propertyInfo.GetValue(dataItem);
|
||||
|
||||
if (isNullable)
|
||||
{
|
||||
builder.OpenComponent<DxSpinEdit<int?>>(seq++);
|
||||
builder.AddAttribute(seq++, "Value", (int?)value);
|
||||
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<int?>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.OpenComponent<DxSpinEdit<int>>(seq++);
|
||||
builder.AddAttribute(seq++, "Value", (int)(value ?? 0));
|
||||
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<int>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
}
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderSpinDecimalEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
|
||||
{
|
||||
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
|
||||
var value = propertyInfo.GetValue(dataItem);
|
||||
|
||||
if (isNullable)
|
||||
{
|
||||
builder.OpenComponent<DxSpinEdit<decimal?>>(seq++);
|
||||
builder.AddAttribute(seq++, "Value", (decimal?)value);
|
||||
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<decimal?>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.OpenComponent<DxSpinEdit<decimal>>(seq++);
|
||||
builder.AddAttribute(seq++, "Value", (decimal)(value ?? 0m));
|
||||
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<decimal>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
}
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderSpinDoubleEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
|
||||
{
|
||||
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
|
||||
var value = propertyInfo.GetValue(dataItem);
|
||||
|
||||
if (isNullable)
|
||||
{
|
||||
builder.OpenComponent<DxSpinEdit<double?>>(seq++);
|
||||
builder.AddAttribute(seq++, "Value", (double?)value);
|
||||
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<double?>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.OpenComponent<DxSpinEdit<double>>(seq++);
|
||||
builder.AddAttribute(seq++, "Value", (double)(value ?? 0d));
|
||||
builder.AddAttribute(seq++, "ValueChanged", EventCallback.Factory.Create<double>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
}
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderTextBoxEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
|
||||
{
|
||||
builder.OpenComponent<DxTextBox>(seq++);
|
||||
builder.AddAttribute(seq++, "Text", propertyInfo.GetValue(dataItem)?.ToString() ?? "");
|
||||
builder.AddAttribute(seq++, "TextChanged", EventCallback.Factory.Create<string>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderMemoEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo)
|
||||
{
|
||||
builder.OpenComponent<DxMemo>(seq++);
|
||||
builder.AddAttribute(seq++, "Text", propertyInfo.GetValue(dataItem)?.ToString() ?? "");
|
||||
builder.AddAttribute(seq++, "TextChanged", EventCallback.Factory.Create<string>(this, v => { propertyInfo.SetValue(dataItem, v); InvokeAsync(StateHasChanged); }));
|
||||
builder.AddAttribute(seq++, "Rows", 3);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderComboBoxEditor(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings)
|
||||
{
|
||||
var value = propertyInfo.GetValue(dataItem);
|
||||
var underlyingType = Nullable.GetUnderlyingType(propertyInfo.PropertyType) ?? propertyInfo.PropertyType;
|
||||
var itemType = settings.Data?.GetType().GetGenericArguments().FirstOrDefault() ?? typeof(object);
|
||||
|
||||
if (underlyingType == typeof(int))
|
||||
RenderComboBoxInt(builder, ref seq, dataItem, propertyInfo, settings, itemType, value);
|
||||
else if (underlyingType == typeof(long))
|
||||
RenderComboBoxLong(builder, ref seq, dataItem, propertyInfo, settings, itemType, value);
|
||||
else if (underlyingType == typeof(Guid))
|
||||
RenderComboBoxGuid(builder, ref seq, dataItem, propertyInfo, settings, itemType, value);
|
||||
else
|
||||
{
|
||||
builder.OpenComponent<DxTextBox>(seq++);
|
||||
builder.AddAttribute(seq++, "Text", ResolveComboBoxDisplayText(settings, value ?? new object()) ?? value?.ToString() ?? "");
|
||||
builder.AddAttribute(seq++, "ReadOnly", true);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderComboBoxInt(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings, Type itemType, object? currentValue)
|
||||
{
|
||||
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
|
||||
var comboType = isNullable ? typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(int?)) : typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(int));
|
||||
|
||||
builder.OpenComponent(seq++, comboType);
|
||||
builder.AddAttribute(seq++, "Data", settings.Data);
|
||||
builder.AddAttribute(seq++, "ValueFieldName", settings.ValueFieldName);
|
||||
builder.AddAttribute(seq++, "TextFieldName", settings.TextFieldName);
|
||||
builder.AddAttribute(seq++, "Value", isNullable ? currentValue as int? : (currentValue is int intVal ? intVal : 0));
|
||||
builder.AddAttribute(seq++, "ValueChanged", isNullable
|
||||
? EventCallback.Factory.Create<int?>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); })
|
||||
: EventCallback.Factory.Create<int>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); }));
|
||||
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderComboBoxLong(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings, Type itemType, object? currentValue)
|
||||
{
|
||||
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
|
||||
var comboType = isNullable ? typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(long?)) : typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(long));
|
||||
|
||||
builder.OpenComponent(seq++, comboType);
|
||||
builder.AddAttribute(seq++, "Data", settings.Data);
|
||||
builder.AddAttribute(seq++, "ValueFieldName", settings.ValueFieldName);
|
||||
builder.AddAttribute(seq++, "TextFieldName", settings.TextFieldName);
|
||||
builder.AddAttribute(seq++, "Value", isNullable ? currentValue as long? : (currentValue is long longVal ? longVal : 0L));
|
||||
builder.AddAttribute(seq++, "ValueChanged", isNullable
|
||||
? EventCallback.Factory.Create<long?>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); })
|
||||
: EventCallback.Factory.Create<long>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); }));
|
||||
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private void RenderComboBoxGuid(RenderTreeBuilder builder, ref int seq, object dataItem, PropertyInfo propertyInfo, DxComboBoxSettings settings, Type itemType, object? currentValue)
|
||||
{
|
||||
var isNullable = Nullable.GetUnderlyingType(propertyInfo.PropertyType) != null;
|
||||
var comboType = isNullable ? typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(Guid?)) : typeof(DxComboBox<,>).MakeGenericType(itemType, typeof(Guid));
|
||||
|
||||
builder.OpenComponent(seq++, comboType);
|
||||
builder.AddAttribute(seq++, "Data", settings.Data);
|
||||
builder.AddAttribute(seq++, "ValueFieldName", settings.ValueFieldName);
|
||||
builder.AddAttribute(seq++, "TextFieldName", settings.TextFieldName);
|
||||
builder.AddAttribute(seq++, "Value", isNullable ? currentValue as Guid? : (currentValue is Guid guidVal ? guidVal : Guid.Empty));
|
||||
builder.AddAttribute(seq++, "ValueChanged", isNullable
|
||||
? EventCallback.Factory.Create<Guid?>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); })
|
||||
: EventCallback.Factory.Create<Guid>(this, v => { propertyInfo.SetValue(dataItem, v); StateHasChanged(); }));
|
||||
builder.AddAttribute(seq++, "ClearButtonDisplayMode", DataEditorClearButtonDisplayMode.Auto);
|
||||
builder.CloseComponent();
|
||||
}
|
||||
|
||||
private RenderFragment RenderCellContent(object? value, string displayText)
|
||||
{
|
||||
return builder =>
|
||||
{
|
||||
var seq = 0;
|
||||
builder.OpenElement(seq++, "span");
|
||||
builder.AddAttribute(seq++, "class", "mg-info-panel-value");
|
||||
builder.AddAttribute(seq++, "title", displayText);
|
||||
|
||||
if (value is bool boolValue)
|
||||
{
|
||||
builder.OpenElement(seq++, "span");
|
||||
builder.AddAttribute(seq++, "class", boolValue ? "dx-icon dx-icon-check" : "dx-icon dx-icon-close");
|
||||
builder.CloseElement();
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.AddContent(seq++, displayText);
|
||||
}
|
||||
|
||||
builder.CloseElement();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,410 @@
|
|||
using DevExpress.Blazor;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
|
||||
namespace AyCode.Blazor.Components.Components.Grids;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for InfoPanel to support grid access
|
||||
/// </summary>
|
||||
public interface IInfoPanelBase
|
||||
{
|
||||
void ClearEditMode();
|
||||
void SetEditMode(object editModel);
|
||||
void RefreshData(IMgGridBase grid, object? dataItem, int visibleIndex = -1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// InfoPanel component for displaying and editing grid row details
|
||||
/// </summary>
|
||||
public partial class MgGridInfoPanel : ComponentBase, IAsyncDisposable, IInfoPanelBase
|
||||
{
|
||||
[Inject] private IJSRuntime JSRuntime { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show readonly fields when in edit mode. Default is false.
|
||||
/// </summary>
|
||||
[Parameter] public bool ShowReadOnlyFieldsInEditMode { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum width for 2 columns layout. Default is 500px.
|
||||
/// </summary>
|
||||
[Parameter] public int TwoColumnBreakpoint { get; set; } = 400;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum width for 3 columns layout. Default is 800px.
|
||||
/// </summary>
|
||||
[Parameter] public int ThreeColumnBreakpoint { get; set; } = 800;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum width for 4 columns layout. Default is 1200px.
|
||||
/// </summary>
|
||||
[Parameter] public int FourColumnBreakpoint { get; set; } = 1300;
|
||||
|
||||
/// <summary>
|
||||
/// Fixed column count. If set (1-4), overrides responsive breakpoints. Default is null (responsive).
|
||||
/// </summary>
|
||||
[Parameter] public int? FixedColumnCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the wrapper component - automatically registers this InfoPanel
|
||||
/// </summary>
|
||||
[CascadingParameter]
|
||||
public MgGridWithInfoPanel? GridWrapper { get; set; }
|
||||
|
||||
private ElementReference _panelElement;
|
||||
private bool _isJsInitialized;
|
||||
private const int DefaultTopOffset = 300; // Increased from 180 to account for header + tabs + toolbar
|
||||
|
||||
protected IMgGridBase? _currentGrid;
|
||||
protected object? _currentDataItem;
|
||||
protected int _focusedRowVisibleIndex = -1;
|
||||
protected List<DxGridDataColumn> _allDataColumns = [];
|
||||
|
||||
// Edit mode state
|
||||
protected bool _isEditMode;
|
||||
protected object? _editModel;
|
||||
|
||||
// Cache for edit settings to avoid repeated lookups
|
||||
private readonly Dictionary<string, IEditSettings?> _editSettingsCache = [];
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
{
|
||||
// Register this InfoPanel with the wrapper
|
||||
GridWrapper?.RegisterInfoPanel(this);
|
||||
|
||||
await InitializeStickyAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InitializeStickyAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync(
|
||||
"MgGridInfoPanel.initSticky",
|
||||
_panelElement,
|
||||
DefaultTopOffset);
|
||||
_isJsInitialized = true;
|
||||
}
|
||||
catch (JSException)
|
||||
{
|
||||
// JS might not be loaded yet, ignore
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the InfoPanel with data from the specified grid row (view mode)
|
||||
/// </summary>
|
||||
public void RefreshData(IMgGridBase grid, object? dataItem, int visibleIndex = -1)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(grid);
|
||||
|
||||
_currentGrid = grid;
|
||||
_currentDataItem = dataItem;
|
||||
_focusedRowVisibleIndex = visibleIndex;
|
||||
_editSettingsCache.Clear();
|
||||
|
||||
// Clear edit mode when refreshing with new data
|
||||
_isEditMode = false;
|
||||
_editModel = null;
|
||||
|
||||
if (_currentGrid != null && _currentDataItem != null)
|
||||
{
|
||||
_allDataColumns = GetAllDataColumns(_currentGrid);
|
||||
}
|
||||
else
|
||||
{
|
||||
_allDataColumns = [];
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
// Notify subscribers that data item changed
|
||||
_ = OnDataItemChanged.InvokeAsync(dataItem);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the InfoPanel to edit mode with the given edit model
|
||||
/// </summary>
|
||||
public void SetEditMode(object editModel)
|
||||
{
|
||||
_editModel = editModel;
|
||||
_isEditMode = true;
|
||||
_currentDataItem = _editModel;
|
||||
|
||||
if (_currentGrid != null)
|
||||
{
|
||||
_allDataColumns = GetAllDataColumns(_currentGrid);
|
||||
}
|
||||
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears edit mode and returns to view mode
|
||||
/// </summary>
|
||||
public void ClearEditMode()
|
||||
{
|
||||
_isEditMode = false;
|
||||
_editModel = null;
|
||||
_editSettingsCache.Clear();
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the InfoPanel completely
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
_currentGrid = null;
|
||||
_currentDataItem = null;
|
||||
_focusedRowVisibleIndex = -1;
|
||||
_allDataColumns = [];
|
||||
_editSettingsCache.Clear();
|
||||
_isEditMode = false;
|
||||
_editModel = null;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_isJsInitialized)
|
||||
{
|
||||
try
|
||||
{
|
||||
await JSRuntime.InvokeVoidAsync("MgGridInfoPanel.disposeSticky", _panelElement);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore disposal errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the data item to display/edit (EditModel in edit mode, otherwise CurrentDataItem)
|
||||
/// </summary>
|
||||
protected object? GetActiveDataItem() => _isEditMode && _editModel != null ? _editModel : _currentDataItem;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the display text for a field using the grid's internal formatting.
|
||||
/// For ComboBox columns, tries to get the text from the lookup data source.
|
||||
/// </summary>
|
||||
protected string GetDisplayTextFromGrid(DxGridDataColumn column)
|
||||
{
|
||||
var dataItem = GetActiveDataItem();
|
||||
if (_currentGrid == null || dataItem == null || string.IsNullOrWhiteSpace(column.FieldName))
|
||||
return string.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
var value = _currentGrid.GetDataItemValue(dataItem, column.FieldName);
|
||||
|
||||
if (value == null)
|
||||
return string.Empty;
|
||||
|
||||
// Try to resolve display text from EditSettings
|
||||
var editSettings = GetEditSettings(column.FieldName);
|
||||
if (editSettings is DxComboBoxSettings comboSettings)
|
||||
{
|
||||
var displayText = ResolveComboBoxDisplayText(comboSettings, value);
|
||||
if (!string.IsNullOrEmpty(displayText))
|
||||
return displayText;
|
||||
}
|
||||
|
||||
// Apply column's DisplayFormat if available
|
||||
if (!string.IsNullOrEmpty(column.DisplayFormat))
|
||||
{
|
||||
try
|
||||
{
|
||||
return string.Format(column.DisplayFormat, value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If format fails, fall through to default formatting
|
||||
}
|
||||
}
|
||||
|
||||
return FormatValue(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets edit settings for the specified field (with caching)
|
||||
/// </summary>
|
||||
private IEditSettings? GetEditSettings(string fieldName)
|
||||
{
|
||||
if (_currentGrid == null || string.IsNullOrEmpty(fieldName))
|
||||
return null;
|
||||
|
||||
if (_editSettingsCache.TryGetValue(fieldName, out var cached))
|
||||
return cached;
|
||||
|
||||
IEditSettings? settings = null;
|
||||
try
|
||||
{
|
||||
// Try each EditSettings type
|
||||
settings = _currentGrid.GetColumnEditSettings<DxComboBoxSettings>(fieldName)
|
||||
?? _currentGrid.GetColumnEditSettings<DxDateEditSettings>(fieldName)
|
||||
?? _currentGrid.GetColumnEditSettings<DxTimeEditSettings>(fieldName)
|
||||
?? _currentGrid.GetColumnEditSettings<DxSpinEditSettings>(fieldName)
|
||||
?? _currentGrid.GetColumnEditSettings<DxCheckBoxSettings>(fieldName)
|
||||
?? _currentGrid.GetColumnEditSettings<DxMemoSettings>(fieldName)
|
||||
?? (IEditSettings?)_currentGrid.GetColumnEditSettings<DxTextBoxSettings>(fieldName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
_editSettingsCache[fieldName] = settings;
|
||||
return settings;
|
||||
}
|
||||
|
||||
private string? ResolveComboBoxDisplayText(DxComboBoxSettings settings, object value)
|
||||
{
|
||||
if (settings.Data == null || string.IsNullOrEmpty(settings.ValueFieldName) || string.IsNullOrEmpty(settings.TextFieldName))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var item in (System.Collections.IEnumerable)settings.Data)
|
||||
{
|
||||
if (item == null) continue;
|
||||
|
||||
var itemType = item.GetType();
|
||||
var valueProperty = itemType.GetProperty(settings.ValueFieldName);
|
||||
var textProperty = itemType.GetProperty(settings.TextFieldName);
|
||||
|
||||
if (valueProperty == null || textProperty == null) continue;
|
||||
|
||||
var itemValue = valueProperty.GetValue(item);
|
||||
if (itemValue != null && itemValue.Equals(value))
|
||||
{
|
||||
return textProperty.GetValue(item)?.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If lookup fails, return null and fall back to default formatting
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string FormatValue(object? value)
|
||||
{
|
||||
if (value == null)
|
||||
return string.Empty;
|
||||
|
||||
return value switch
|
||||
{
|
||||
DateTime dateTime => dateTime.ToString("yyyy-MM-dd HH:mm:ss"),
|
||||
DateOnly dateOnly => dateOnly.ToString("yyyy-MM-dd"),
|
||||
TimeOnly timeOnly => timeOnly.ToString("HH:mm:ss"),
|
||||
TimeSpan timeSpan => timeSpan.ToString(@"hh\:mm\:ss"),
|
||||
bool boolValue => boolValue ? "Igen" : "Nem",
|
||||
decimal decValue => decValue.ToString("N2"),
|
||||
double dblValue => dblValue.ToString("N2"),
|
||||
float fltValue => fltValue.ToString("N2"),
|
||||
int or long or short or byte => $"{value:N0}",
|
||||
_ => value.ToString() ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the columns to display based on edit mode and ShowReadOnlyFieldsInEditMode setting
|
||||
/// </summary>
|
||||
protected IEnumerable<DxGridDataColumn> GetVisibleColumns()
|
||||
{
|
||||
if (!_isEditMode || ShowReadOnlyFieldsInEditMode)
|
||||
{
|
||||
return _allDataColumns;
|
||||
}
|
||||
|
||||
// In edit mode with ShowReadOnlyFieldsInEditMode=false, hide readonly columns
|
||||
return _allDataColumns.Where(c => !c.ReadOnly);
|
||||
}
|
||||
|
||||
protected object? GetCellValue(DxGridDataColumn column)
|
||||
{
|
||||
var dataItem = GetActiveDataItem();
|
||||
if (_currentGrid == null || dataItem == null || string.IsNullOrWhiteSpace(column.FieldName))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
return _currentGrid.GetDataItemValue(dataItem, column.FieldName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected static List<DxGridDataColumn> GetAllDataColumns(IMgGridBase grid)
|
||||
{
|
||||
var columns = new List<DxGridDataColumn>();
|
||||
|
||||
try
|
||||
{
|
||||
var allColumns = grid.GetDataColumns();
|
||||
|
||||
if (allColumns != null)
|
||||
{
|
||||
foreach (var column in allColumns)
|
||||
{
|
||||
if (column is DxGridDataColumn dataColumn &&
|
||||
!string.IsNullOrWhiteSpace(dataColumn.FieldName))
|
||||
{
|
||||
columns.Add(dataColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Ignore errors
|
||||
}
|
||||
|
||||
return columns;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the EditSettings type for rendering logic
|
||||
/// </summary>
|
||||
private EditSettingsType GetEditSettingsType(DxGridDataColumn column)
|
||||
{
|
||||
var settings = GetEditSettings(column.FieldName);
|
||||
|
||||
return settings switch
|
||||
{
|
||||
DxComboBoxSettings => EditSettingsType.ComboBox,
|
||||
DxDateEditSettings => EditSettingsType.DateEdit,
|
||||
DxTimeEditSettings => EditSettingsType.TimeEdit,
|
||||
DxSpinEditSettings => EditSettingsType.SpinEdit,
|
||||
DxCheckBoxSettings => EditSettingsType.CheckBox,
|
||||
DxMemoSettings => EditSettingsType.Memo,
|
||||
_ => EditSettingsType.None
|
||||
};
|
||||
}
|
||||
|
||||
private enum EditSettingsType
|
||||
{
|
||||
None,
|
||||
ComboBox,
|
||||
DateEdit,
|
||||
TimeEdit,
|
||||
SpinEdit,
|
||||
CheckBox,
|
||||
Memo
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
/* Shared edit mode background color configuration - change only here */
|
||||
/* Grid row background: #fffbeb (see MgGridBase.cs OnCustomizeElement) */
|
||||
/* InfoPanel background: #fffbeb (see below .edit-mode) */
|
||||
/* Border color: #f59e0b */
|
||||
|
||||
/* Main panel - contained within splitter pane */
|
||||
.mg-grid-info-panel {
|
||||
container-type: inline-size;
|
||||
container-name: infopanel;
|
||||
background-color: var(--dxbl-bg-secondary, #f8f9fa);
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
/* Prevent panel from pushing out the splitter */
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
/* Default breakpoints - can be overridden via style attribute */
|
||||
--mg-bp-2col: 500px;
|
||||
--mg-bp-3col: 800px;
|
||||
--mg-bp-4col: 1200px;
|
||||
}
|
||||
|
||||
.mg-grid-info-panel.edit-mode {
|
||||
background-color: #fffbeb !important;
|
||||
border-left: 3px solid #f59e0b !important;
|
||||
}
|
||||
|
||||
.mg-grid-info-panel.view-mode {
|
||||
background-color: #f8f9fa !important;
|
||||
border-left: 3px solid transparent !important;
|
||||
}
|
||||
|
||||
/* Content area - scrollable, takes remaining space */
|
||||
.mg-info-panel-content {
|
||||
flex: 1 1 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 1rem;
|
||||
min-height: 0; /* Critical for flex child to allow shrinking */
|
||||
}
|
||||
|
||||
/* Grid layout with responsive column wrapping based on panel width */
|
||||
.mg-info-panel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
/* Fixed column count classes - override responsive behavior */
|
||||
.mg-columns-1 .mg-info-panel-grid {
|
||||
grid-template-columns: 1fr !important;
|
||||
}
|
||||
|
||||
.mg-columns-2 .mg-info-panel-grid {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
}
|
||||
|
||||
.mg-columns-3 .mg-info-panel-grid {
|
||||
grid-template-columns: repeat(3, 1fr) !important;
|
||||
}
|
||||
|
||||
.mg-columns-4 .mg-info-panel-grid {
|
||||
grid-template-columns: repeat(4, 1fr) !important;
|
||||
}
|
||||
|
||||
/* Responsive layouts using container queries (when no fixed column count) */
|
||||
/* 1 column for narrow panels (< 2col breakpoint) - default above */
|
||||
|
||||
/* 2 columns for medium width */
|
||||
@container infopanel (min-width: 500px) {
|
||||
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* 3 columns for wider panels */
|
||||
@container infopanel (min-width: 800px) {
|
||||
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* 4 columns for very wide panels */
|
||||
@container infopanel (min-width: 1200px) {
|
||||
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.mg-info-panel-item {
|
||||
min-width: 0; /* Prevent grid blowout */
|
||||
}
|
||||
|
||||
/* Fallback styles */
|
||||
.info-panel-form {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info-panel-form .fw-semibold {
|
||||
font-weight: 600;
|
||||
color: var(--dxbl-text-secondary, #495057);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.info-panel-form .fw-semibold.text-primary {
|
||||
color: var(--dxbl-primary, #0d6efd);
|
||||
}
|
||||
|
||||
/* Text overflow handling - show ellipsis and full text in tooltip */
|
||||
.info-panel-text-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info-panel-text-wrapper input[readonly] {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* View mode value styling - matches DevExpress theme */
|
||||
.mg-info-panel-value {
|
||||
padding: 0.375rem 0.75rem;
|
||||
min-height: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
background-color: var(--dxbl-bg, #fff);
|
||||
border: 1px solid var(--dxbl-border-color, #dee2e6);
|
||||
border-radius: var(--dxbl-border-radius, 0.25rem);
|
||||
font-size: var(--dxbl-font-size, 0.875rem);
|
||||
color: var(--dxbl-text, #212529);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mg-info-panel-value-numeric {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.mg-info-panel-value-bool {
|
||||
/* Keep left aligned */
|
||||
}
|
||||
|
||||
.mg-info-panel-value-date {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace AyCode.Blazor.Components.Components.Grids;
|
||||
|
||||
public class MgGridInfoPanelHelper
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
using DevExpress.Blazor;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
||||
namespace AyCode.Blazor.Components.Components.Grids
|
||||
{
|
||||
public class MgGridToolbarBase : DxToolbar
|
||||
{
|
||||
[Parameter] public IMgGridBase Grid { get; set; }
|
||||
[Parameter] public Func<ToolbarItemClickEventArgs, Task> RefreshClick { get; set; }
|
||||
[Parameter] public bool ShowOnlyIcon { get; set; } = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace AyCode.Blazor.Components.Components.Grids;
|
||||
|
||||
public class MgGridToolbarHelper
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,154 @@
|
|||
@using AyCode.Blazor.Components.Components.Grids
|
||||
|
||||
<MgGridToolbarBase @ref="GridToolbar" Grid="Grid" ItemRenderStyleMode="ToolbarRenderStyleMode.Plain" ShowOnlyIcon="ShowOnlyIcon">
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "New")" Click="NewItem_Click" IconCssClass="grid-new-row" Visible="@(!IsEditing)" Enabled="@(!IsSyncing)" />
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Edit")" Click="EditItem_Click" IconCssClass="grid-edit-row" Visible="@(!IsEditing)" Enabled="@(HasFocusedRow && !IsSyncing)" />
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Delete")" Click="DeleteItem_Click" IconCssClass="grid-delete-row" Visible="@(!IsEditing)" Enabled="@(false && HasFocusedRow && !IsSyncing)" />
|
||||
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Save")" Click="SaveItem_Click" IconCssClass="grid-save" Visible="@IsEditing" RenderStyle="ButtonRenderStyle.Primary" />
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Cancel")" Click="CancelEdit_Click" IconCssClass="grid-cancel" Visible="@IsEditing" RenderStyle="ButtonRenderStyle.Secondary" />
|
||||
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Prev Row")" BeginGroup="true" Click="PrevRow_Click" IconCssClass="grid-chevron-up" Enabled="@(HasFocusedRow && !IsSyncing && !IsEditing)" />
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Next Row")" Click="NextRow_Click" IconCssClass="grid-chevron-down" Enabled="@(HasFocusedRow && !IsSyncing && !IsEditing)" />
|
||||
|
||||
@if (!OnlyGridEditTools)
|
||||
{
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Column Chooser")" BeginGroup="true" Click="ColumnChooserItem_Click" IconCssClass="grid-column-chooser" />
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Export")" IconCssClass="grid-export" Visible="false" Enabled="@(HasFocusedRow && !IsEditing)">
|
||||
<Items>
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "To CSV")" Click="ExportCsvItem_Click" IconCssClass="grid-export-xlsx" />
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "To XLSX")" Click="ExportXlsxItem_Click" IconCssClass="grid-export-xlsx" />
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "To XLS")" Click="ExportXlsItem_Click" IconCssClass="grid-export-xlsx" />
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "To PDF")" Click="ExportPdfItem_Click" IconCssClass="grid-export-pdf" />
|
||||
</Items>
|
||||
</DxToolbarItem>
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : "Reload data")" BeginGroup="true" Click="ReloadData_Click" IconCssClass="grid-refresh" Enabled="@(!IsSyncing && !_isReloadInProgress && !IsEditing)" />
|
||||
<DxToolbarItem Text="@(ShowOnlyIcon ? "" : FullscreenButtonText)" Click="Fullscreen_Click" IconCssClass="@FullscreenIconCssClass" Enabled="@(!IsEditing)" />
|
||||
@ToolbarItemsExtended
|
||||
}
|
||||
</MgGridToolbarBase>
|
||||
|
||||
@code {
|
||||
[Parameter] public bool OnlyGridEditTools { get; set; } = false;
|
||||
[Parameter] public IMgGridBase Grid { get; set; } = null!;
|
||||
[Parameter] public RenderFragment? ToolbarItemsExtended { get; set; }
|
||||
[Parameter] public EventCallback<ToolbarItemClickEventArgs> OnReloadDataClick { get; set; }
|
||||
[Parameter] public bool ShowOnlyIcon { get; set; } = false;
|
||||
|
||||
public MgGridToolbarBase GridToolbar { get; set; } = null!;
|
||||
const string ExportFileName = "ExportResult";
|
||||
|
||||
private bool _isReloadInProgress;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the grid is currently in edit mode (New or Edit)
|
||||
/// </summary>
|
||||
private bool IsEditing => Grid?.GridEditState != MgGridEditState.None;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the grid is currently syncing data
|
||||
/// </summary>
|
||||
private bool IsSyncing => Grid?.IsSyncing ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Whether there is a focused row in the grid
|
||||
/// </summary>
|
||||
private bool HasFocusedRow => Grid?.GetFocusedRowIndex() >= 0;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the grid is currently in fullscreen mode
|
||||
/// </summary>
|
||||
private bool IsFullscreenMode => Grid?.IsFullscreen ?? false;
|
||||
|
||||
/// <summary>
|
||||
/// Button text for fullscreen toggle
|
||||
/// </summary>
|
||||
private string FullscreenButtonText => IsFullscreenMode ? "Exit Fullscreen" : "Fullscreen";
|
||||
|
||||
/// <summary>
|
||||
/// Icon class for fullscreen toggle button
|
||||
/// </summary>
|
||||
private string FullscreenIconCssClass => IsFullscreenMode ? "grid-fullscreen-exit" : "grid-fullscreen";
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
}
|
||||
|
||||
async Task ReloadData_Click(ToolbarItemClickEventArgs e)
|
||||
{
|
||||
_isReloadInProgress = true;
|
||||
try
|
||||
{
|
||||
await OnReloadDataClick.InvokeAsync(e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isReloadInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
async Task NewItem_Click()
|
||||
{
|
||||
await Grid.StartEditNewRowAsync();
|
||||
}
|
||||
|
||||
async Task EditItem_Click()
|
||||
{
|
||||
await Grid.StartEditRowAsync(Grid.GetFocusedRowIndex());
|
||||
}
|
||||
|
||||
void DeleteItem_Click()
|
||||
{
|
||||
Grid.ShowRowDeleteConfirmation(Grid.GetFocusedRowIndex());
|
||||
}
|
||||
|
||||
async Task SaveItem_Click()
|
||||
{
|
||||
await Grid.SaveChangesAsync();
|
||||
}
|
||||
|
||||
async Task CancelEdit_Click()
|
||||
{
|
||||
await Grid.CancelEditAsync();
|
||||
}
|
||||
|
||||
void PrevRow_Click()
|
||||
{
|
||||
Grid.StepPrevRow();
|
||||
}
|
||||
|
||||
void NextRow_Click()
|
||||
{
|
||||
Grid.StepNextRow();
|
||||
}
|
||||
|
||||
void ColumnChooserItem_Click(ToolbarItemClickEventArgs e)
|
||||
{
|
||||
Grid.ShowColumnChooser();
|
||||
}
|
||||
|
||||
void Fullscreen_Click()
|
||||
{
|
||||
Grid.ToggleFullscreen();
|
||||
}
|
||||
|
||||
async Task ExportXlsxItem_Click()
|
||||
{
|
||||
await Grid.ExportToXlsxAsync(ExportFileName);
|
||||
}
|
||||
|
||||
async Task ExportXlsItem_Click()
|
||||
{
|
||||
await Grid.ExportToXlsAsync(ExportFileName);
|
||||
}
|
||||
|
||||
async Task ExportCsvItem_Click()
|
||||
{
|
||||
await Grid.ExportToCsvAsync(ExportFileName);
|
||||
}
|
||||
|
||||
async Task ExportPdfItem_Click()
|
||||
{
|
||||
await Grid.ExportToPdfAsync(ExportFileName);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
@using DevExpress.Blazor
|
||||
|
||||
<CascadingValue Value="this">
|
||||
@if (_isFullscreen)
|
||||
{
|
||||
<div class="mg-fullscreen-overlay">
|
||||
<div class="mg-fullscreen-header">
|
||||
<span class="mg-fullscreen-title">@(_currentGrid?.Caption ?? "Grid")</span>
|
||||
<button type="button" class="btn-close btn-close-white" aria-label="Close" @onclick="ExitFullscreen"></button>
|
||||
</div>
|
||||
<div class="mg-fullscreen-body">
|
||||
@RenderMainContent()
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@RenderMainContent()
|
||||
}
|
||||
</CascadingValue>
|
||||
|
||||
@code {
|
||||
private IInfoPanelBase? _infoPanelInstance;
|
||||
private IMgGridBase? _currentGrid;
|
||||
private bool _isFullscreen;
|
||||
|
||||
/// <summary>
|
||||
/// The grid content to display in the left pane
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment? GridContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// InfoPanel content (e.g., GridShippingDocumentInfoPanel) to display in the right pane.
|
||||
/// If not set, the default MgGridInfoPanel is used.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initial size of the InfoPanel pane. Default is "400px".
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public string InfoPanelSize { get; set; } = "400px";
|
||||
|
||||
/// <summary>
|
||||
/// Whether to show the InfoPanel. Default is true.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
public bool ShowInfoPanel { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the wrapper is currently in fullscreen mode
|
||||
/// </summary>
|
||||
public bool IsFullscreen => _isFullscreen;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the InfoPanel instance for grid-InfoPanel communication
|
||||
/// </summary>
|
||||
public IInfoPanelBase? InfoPanelInstance
|
||||
{
|
||||
get => _infoPanelInstance;
|
||||
set => _infoPanelInstance = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers an InfoPanel instance (called by child InfoPanel components)
|
||||
/// </summary>
|
||||
public void RegisterInfoPanel(IInfoPanelBase infoPanel)
|
||||
{
|
||||
_infoPanelInstance = infoPanel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the grid instance (called by MgGridBase)
|
||||
/// </summary>
|
||||
public void RegisterGrid(IMgGridBase grid)
|
||||
{
|
||||
_currentGrid = grid;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles fullscreen mode
|
||||
/// </summary>
|
||||
public void ToggleFullscreen()
|
||||
{
|
||||
_isFullscreen = !_isFullscreen;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enters fullscreen mode
|
||||
/// </summary>
|
||||
public void EnterFullscreen()
|
||||
{
|
||||
_isFullscreen = true;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exits fullscreen mode
|
||||
/// </summary>
|
||||
public void ExitFullscreen()
|
||||
{
|
||||
_isFullscreen = false;
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
private RenderFragment RenderMainContent() => __builder =>
|
||||
{
|
||||
if (ShowInfoPanel)
|
||||
{
|
||||
<DxSplitter Width="100%" Height="@(_isFullscreen ? "100%" : null)" CssClass="mg-grid-with-info-panel" Orientation="Orientation.Horizontal">
|
||||
<Panes>
|
||||
<DxSplitterPane>
|
||||
@GridContent
|
||||
</DxSplitterPane>
|
||||
<DxSplitterPane Size="@InfoPanelSize" MinSize="0px" MaxSize="100%" AllowCollapse="true" CssClass="mg-info-panel-pane">
|
||||
@if (ChildContent != null)
|
||||
{
|
||||
@ChildContent
|
||||
}
|
||||
else
|
||||
{
|
||||
<MgGridInfoPanel />
|
||||
}
|
||||
</DxSplitterPane>
|
||||
</Panes>
|
||||
</DxSplitter>
|
||||
}
|
||||
else
|
||||
{
|
||||
@GridContent
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
namespace AyCode.Blazor.Components.Components;
|
||||
|
||||
public class MgComponentsHelper
|
||||
{
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
/* MgGridInfoPanel styles - DevExpress theme compatible */
|
||||
|
||||
/* Main panel - uses DevExpress theme variables */
|
||||
.mg-grid-info-panel {
|
||||
container-type: inline-size;
|
||||
container-name: infopanel;
|
||||
background-color: var(--dxbl-bg-secondary);
|
||||
color: var(--dxbl-text);
|
||||
font-family: var(--dxbl-font-family);
|
||||
font-size: var(--dxbl-font-size);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
max-height: 100%;
|
||||
border-left: 1px solid var(--dxbl-border-color);
|
||||
}
|
||||
|
||||
.mg-grid-info-panel.edit-mode {
|
||||
background-color: var(--dxbl-warning-bg, #fffbeb);
|
||||
border-left: 3px solid var(--dxbl-warning, #f59e0b);
|
||||
}
|
||||
|
||||
/* Header styling */
|
||||
.mg-grid-info-panel .mg-info-panel-header {
|
||||
padding: var(--dxbl-spacer-sm) var(--dxbl-spacer);
|
||||
background-color: var(--dxbl-bg);
|
||||
border-bottom: 1px solid var(--dxbl-border-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Toolbar styling */
|
||||
.mg-info-panel-toolbar {
|
||||
padding: var(--dxbl-spacer-xs) var(--dxbl-spacer-sm);
|
||||
background-color: var(--dxbl-bg);
|
||||
border-bottom: 1px solid var(--dxbl-border-color);
|
||||
}
|
||||
|
||||
/* Content area - scrollable */
|
||||
.mg-info-panel-content {
|
||||
flex: 1 1 0;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: var(--dxbl-spacer);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Grid layout for columns */
|
||||
.mg-info-panel-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: var(--dxbl-spacer-sm);
|
||||
}
|
||||
|
||||
/* Fixed column count classes */
|
||||
.mg-columns-1 .mg-info-panel-grid { grid-template-columns: 1fr !important; }
|
||||
.mg-columns-2 .mg-info-panel-grid { grid-template-columns: repeat(2, 1fr) !important; }
|
||||
.mg-columns-3 .mg-info-panel-grid { grid-template-columns: repeat(3, 1fr) !important; }
|
||||
.mg-columns-4 .mg-info-panel-grid { grid-template-columns: repeat(4, 1fr) !important; }
|
||||
|
||||
/* Responsive layouts using container queries */
|
||||
@container infopanel (min-width: 400px) {
|
||||
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@container infopanel (min-width: 800px) {
|
||||
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@container infopanel (min-width: 1300px) {
|
||||
.mg-grid-info-panel:not(.mg-columns-1):not(.mg-columns-2):not(.mg-columns-3):not(.mg-columns-4) .mg-info-panel-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* Grid item */
|
||||
.mg-info-panel-item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Label styling */
|
||||
.mg-info-panel-label {
|
||||
display: block;
|
||||
margin-bottom: var(--dxbl-spacer-xs);
|
||||
font-size: calc(var(--dxbl-font-size) * 0.875);
|
||||
font-weight: 600;
|
||||
color: var(--dxbl-text-secondary);
|
||||
}
|
||||
|
||||
.mg-info-panel-label.editable {
|
||||
color: var(--dxbl-primary);
|
||||
}
|
||||
|
||||
/* View mode value styling */
|
||||
.mg-info-panel-value {
|
||||
display: block;
|
||||
padding: var(--dxbl-spacer-xs) 0;
|
||||
color: var(--dxbl-text);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mg-info-panel-value-numeric {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.mg-info-panel-value-date {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.mg-info-panel-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: var(--dxbl-text-muted);
|
||||
padding: var(--dxbl-spacer-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Tables inside info panel */
|
||||
.mg-info-panel-content table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--dxbl-font-size);
|
||||
color: var(--dxbl-text);
|
||||
margin-bottom: var(--dxbl-spacer);
|
||||
}
|
||||
|
||||
.mg-info-panel-content table th,
|
||||
.mg-info-panel-content table td {
|
||||
padding: var(--dxbl-spacer-xs) var(--dxbl-spacer-sm);
|
||||
border: 1px solid var(--dxbl-border-color);
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mg-info-panel-content table th {
|
||||
background-color: var(--dxbl-bg-secondary);
|
||||
font-weight: 600;
|
||||
color: var(--dxbl-text-secondary);
|
||||
}
|
||||
|
||||
.mg-info-panel-content table tbody tr:nth-child(odd) {
|
||||
background-color: var(--dxbl-bg);
|
||||
}
|
||||
|
||||
.mg-info-panel-content table tbody tr:nth-child(even) {
|
||||
background-color: var(--dxbl-bg-secondary);
|
||||
}
|
||||
|
||||
.mg-info-panel-content table tbody tr:hover {
|
||||
background-color: var(--dxbl-row-hover-bg);
|
||||
}
|
||||
|
||||
/* Splitter pane styling */
|
||||
.mg-grid-with-info-panel {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mg-info-panel-pane {
|
||||
background-color: var(--dxbl-bg-secondary);
|
||||
}
|
||||
|
||||
/* Fullscreen overlay styling (Bootstrap 5 based) */
|
||||
.mg-fullscreen-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: 1050;
|
||||
background-color: var(--dxbl-bg, #fff);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mg-fullscreen-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 1rem;
|
||||
background-color: var(--dxbl-primary, #0d6efd);
|
||||
color: #fff;
|
||||
border-bottom: 1px solid var(--dxbl-border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mg-fullscreen-title {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mg-fullscreen-header .btn-close-white {
|
||||
filter: brightness(0) invert(1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.mg-fullscreen-header .btn-close-white:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mg-fullscreen-body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mg-fullscreen-body .mg-grid-with-info-panel,
|
||||
.mg-fullscreen-body .dxbl-grid {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Legacy DxWindow styling (kept for backwards compatibility) */
|
||||
.mg-fullscreen-window {
|
||||
position: fixed !important;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
margin: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.mg-fullscreen-window .dxbl-window-body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mg-fullscreen-content {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mg-fullscreen-content .mg-grid-with-info-panel,
|
||||
.mg-fullscreen-content .dxbl-grid {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Fullscreen icon classes */
|
||||
.grid-fullscreen::before {
|
||||
content: "\e90c";
|
||||
font-family: 'devextreme-icons';
|
||||
}
|
||||
|
||||
.grid-fullscreen-exit::before {
|
||||
content: "\e90d";
|
||||
font-family: 'devextreme-icons';
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
// MgGridInfoPanel - Sticky scroll handling
|
||||
// Makes the InfoPanel sticky to viewport when scrolling
|
||||
|
||||
window.MgGridInfoPanel = {
|
||||
observers: new Map(),
|
||||
|
||||
// Initialize sticky behavior for an InfoPanel element
|
||||
initSticky: function (element, topOffset) {
|
||||
if (!element) return;
|
||||
|
||||
const elementId = element.id || this.generateId(element);
|
||||
|
||||
// Clean up existing observer if any
|
||||
this.disposeSticky(element);
|
||||
|
||||
// Store the initial position of the element (relative to document)
|
||||
const rect = element.getBoundingClientRect();
|
||||
const initialTop = rect.top + window.scrollY;
|
||||
|
||||
// Calculate and set initial state
|
||||
this.updatePosition(element, initialTop);
|
||||
|
||||
// Handler to update position on scroll and resize
|
||||
const updateHandler = () => {
|
||||
this.updatePosition(element, initialTop);
|
||||
};
|
||||
|
||||
// Add event listeners - use passive to not block scrolling
|
||||
window.addEventListener('resize', updateHandler, { passive: true });
|
||||
window.addEventListener('scroll', updateHandler, { passive: true });
|
||||
|
||||
// Store cleanup info
|
||||
this.observers.set(elementId, {
|
||||
element: element,
|
||||
updateHandler: updateHandler,
|
||||
initialTop: initialTop
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
// Dispose sticky behavior
|
||||
disposeSticky: function (element) {
|
||||
if (!element) return;
|
||||
|
||||
const elementId = element.id || this.findElementId(element);
|
||||
const observer = this.observers.get(elementId);
|
||||
|
||||
if (observer) {
|
||||
window.removeEventListener('resize', observer.updateHandler);
|
||||
window.removeEventListener('scroll', observer.updateHandler);
|
||||
|
||||
// Reset styles
|
||||
element.style.height = '';
|
||||
element.style.maxHeight = '';
|
||||
element.style.transform = '';
|
||||
|
||||
this.observers.delete(elementId);
|
||||
}
|
||||
},
|
||||
|
||||
// Update panel position and height based on scroll
|
||||
updatePosition: function (element, initialTop) {
|
||||
if (!element) return;
|
||||
|
||||
const scrollY = window.scrollY;
|
||||
const viewportHeight = window.innerHeight;
|
||||
const bottomPadding = 30; // 30px from bottom
|
||||
|
||||
// Calculate how much we've scrolled past the initial position
|
||||
const scrolledPast = Math.max(0, scrollY - initialTop);
|
||||
|
||||
// Get the splitter pane to know our container limits
|
||||
const paneContainer = element.closest('.dxbl-splitter-pane');
|
||||
let maxScrollOffset = Infinity;
|
||||
|
||||
if (paneContainer) {
|
||||
// Don't scroll past the bottom of the pane
|
||||
const paneHeight = paneContainer.offsetHeight;
|
||||
const elementHeight = element.offsetHeight;
|
||||
maxScrollOffset = Math.max(0, paneHeight - elementHeight);
|
||||
}
|
||||
|
||||
// Clamp the scroll offset
|
||||
const translateY = Math.min(scrolledPast, maxScrollOffset);
|
||||
|
||||
// Apply transform to make it "sticky"
|
||||
element.style.transform = `translateY(${translateY}px)`;
|
||||
|
||||
// Calculate height: from current visual position to viewport bottom
|
||||
const rect = element.getBoundingClientRect();
|
||||
const visualTop = rect.top; // This already accounts for transform
|
||||
|
||||
// Height from current visual top to viewport bottom minus padding
|
||||
const availableHeight = viewportHeight - visualTop - bottomPadding;
|
||||
|
||||
// Clamp height
|
||||
const finalHeight = Math.max(200, Math.min(availableHeight, viewportHeight - bottomPadding));
|
||||
|
||||
element.style.height = finalHeight + 'px';
|
||||
element.style.maxHeight = finalHeight + 'px';
|
||||
},
|
||||
|
||||
// Generate a unique ID for the element
|
||||
generateId: function (element) {
|
||||
const id = 'mg-info-panel-' + Math.random().toString(36).substr(2, 9);
|
||||
element.id = id;
|
||||
return id;
|
||||
},
|
||||
|
||||
// Find element ID from stored observers
|
||||
findElementId: function (element) {
|
||||
for (const [id, observer] of this.observers.entries()) {
|
||||
if (observer.element === element) {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
Loading…
Reference in New Issue