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 AyCode.Utils.Extensions;
|
||||||
using DevExpress.Blazor;
|
using DevExpress.Blazor;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using Microsoft.AspNetCore.Components.Rendering;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using DevExpress.Blazor.Internal;
|
||||||
|
|
||||||
namespace AyCode.Blazor.Components.Components.Grids;
|
namespace AyCode.Blazor.Components.Components.Grids;
|
||||||
|
|
||||||
|
|
@ -20,10 +22,47 @@ public interface IMgGridBase : IGrid
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsSyncing { get; }
|
bool IsSyncing { get; }
|
||||||
|
|
||||||
|
string Caption { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Event fired when synchronization state changes (true = syncing started, false = syncing ended)
|
/// Current edit state of the grid (None, New, Edit)
|
||||||
/// </summary>
|
/// </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
|
public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClient> : DxGrid, IMgGridBase, IAsyncDisposable
|
||||||
|
|
@ -47,12 +86,118 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
||||||
public bool IsSyncing => _dataSource?.IsSyncing ?? false;
|
public bool IsSyncing => _dataSource?.IsSyncing ?? false;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public event Action<bool>? OnSyncingStateChanged;
|
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 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()
|
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(TDataItem dataItem) => HasIdValue(dataItem.Id);
|
||||||
protected bool HasIdValue(TId id) => !_equalityComparerId.Equals(id, default);
|
protected bool HasIdValue(TId id) => !_equalityComparerId.Equals(id, default);
|
||||||
protected bool IdEquals(TId id1, TId id2) => _equalityComparerId.Equals(id1, id2);
|
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 string? KeyFieldNameToParentId { get; set; }
|
||||||
[Parameter] public object[]? ContextIds { get; set; }
|
[Parameter] public object[]? ContextIds { get; set; }
|
||||||
|
|
||||||
|
[Parameter] public string Caption { get; set; } = typeof(TDataItem).Name;
|
||||||
|
|
||||||
public bool IsMasterGrid => ParentDataItem == null;
|
public bool IsMasterGrid => ParentDataItem == null;
|
||||||
protected PropertyInfo? KeyFieldPropertyInfoToParent;
|
protected PropertyInfo? KeyFieldPropertyInfoToParent;
|
||||||
|
|
||||||
|
|
@ -104,6 +251,9 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
||||||
protected new EventCallback<GridCustomizeEditModelEventArgs> CustomizeEditModel { get; set; }
|
protected new EventCallback<GridCustomizeEditModelEventArgs> CustomizeEditModel { get; set; }
|
||||||
[Parameter] public EventCallback<GridCustomizeEditModelEventArgs> OnGridCustomizeEditModel { 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<IList<TDataItem>> OnDataSourceChanged { get; set; }
|
||||||
[Parameter] public EventCallback<GridDataItemChangingEventArgs<TDataItem>> OnGridItemChanging { 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 = (TSignalRDataSource)Activator.CreateInstance(typeof(TSignalRDataSource), SignalRClient, crudTags, ContextIds)!;
|
||||||
_dataSource.FilterText = FilterText;
|
_dataSource.FilterText = FilterText;
|
||||||
//_dataSource = new SignalRDataSource<TDataItem>(SignalRClient, crudTags, ContextIds) { FilterText = FilterText };
|
|
||||||
|
|
||||||
SetGridData(_dataSource.GetReferenceInnerList());
|
SetGridData(_dataSource.GetReferenceInnerList());
|
||||||
|
|
||||||
|
|
@ -187,7 +336,7 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
||||||
if (_isDisposed) return;
|
if (_isDisposed) return;
|
||||||
|
|
||||||
// Forward the event to external subscribers
|
// Forward the event to external subscribers
|
||||||
OnSyncingStateChanged?.Invoke(isSyncing);
|
//OnSyncingStateChanged?.Invoke(isSyncing);
|
||||||
|
|
||||||
// Trigger UI update
|
// Trigger UI update
|
||||||
InvokeAsync(StateHasChanged);
|
InvokeAsync(StateHasChanged);
|
||||||
|
|
@ -217,12 +366,8 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
||||||
|
|
||||||
Logger.Debug($"{_gridLogName} OnDataSourceLoaded; Count: {_dataSource?.Count}");
|
Logger.Debug($"{_gridLogName} OnDataSourceLoaded; Count: {_dataSource?.Count}");
|
||||||
|
|
||||||
//if(_dataSourceParam.GetType() == typeof()AcObservableCollection<TDataItem>)
|
|
||||||
|
|
||||||
SetGridData(_dataSource!.GetReferenceInnerList());
|
SetGridData(_dataSource!.GetReferenceInnerList());
|
||||||
//else Reload();
|
|
||||||
|
|
||||||
//_dataSource.LoadItem(_dataSource.First().Id).Forget();
|
|
||||||
if (!_isDisposed)
|
if (!_isDisposed)
|
||||||
{
|
{
|
||||||
await OnDataSourceChanged.InvokeAsync(_dataSource);
|
await OnDataSourceChanged.InvokeAsync(_dataSource);
|
||||||
|
|
@ -305,29 +450,44 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
||||||
e.EditModel = editModel;
|
e.EditModel = editModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set edit state
|
||||||
|
GridEditState = e.IsNew ? MgGridEditState.New : MgGridEditState.Edit;
|
||||||
|
|
||||||
await OnGridCustomizeEditModel.InvokeAsync(e);
|
await OnGridCustomizeEditModel.InvokeAsync(e);
|
||||||
|
|
||||||
|
// Update InfoPanel to edit mode
|
||||||
|
InfoPanelInstance?.SetEditMode(editModel);
|
||||||
|
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnEditStart(GridEditStartEventArgs e)
|
private async Task OnEditStart(GridEditStartEventArgs e)
|
||||||
{
|
{
|
||||||
var dataItem = (e.DataItem as TDataItem)!;
|
|
||||||
|
|
||||||
await OnGridEditStart.InvokeAsync(e);
|
await OnGridEditStart.InvokeAsync(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
//void Grid_CustomizeEditModel(GridCustomizeEditModelEventArgs e)
|
protected virtual async Task OnFocusedRowChanged(GridFocusedRowChangedEventArgs e)
|
||||||
//{
|
{
|
||||||
// var model = e.EditModel as EditableWorkOrder;
|
_focusedDataItem = e.DataItem;
|
||||||
// if (model == null)
|
|
||||||
// {
|
|
||||||
// model = new EditableWorkOrder();
|
|
||||||
|
|
||||||
// model.WorkOrderNum = "123";
|
var infoPanelInstance = InfoPanelInstance;
|
||||||
// model.Description = "hey";
|
|
||||||
|
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)
|
private async Task OnItemSaving(GridEditModelSavingEventArgs e)
|
||||||
{
|
{
|
||||||
var dataItem = (e.EditModel as TDataItem)!;
|
var dataItem = (e.EditModel as TDataItem)!;
|
||||||
|
|
@ -335,15 +495,6 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
||||||
if (e.IsNew)
|
if (e.IsNew)
|
||||||
{
|
{
|
||||||
if (!HasIdValue(dataItem)) SetNewId(dataItem);
|
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";
|
var logText = e.IsNew ? "add" : "update";
|
||||||
|
|
@ -364,11 +515,20 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
||||||
}
|
}
|
||||||
else await UpdateDataItemAsync(dataItem);
|
else await UpdateDataItemAsync(dataItem);
|
||||||
|
|
||||||
//var equalityComparer = EqualityComparer<TId>.Default;
|
GridEditState = MgGridEditState.None;
|
||||||
//var index = CollectionExtensions.FindIndex(_dataSource, x => equalityComparer.Equals(x.Id, dataItem.Id));
|
|
||||||
//_dataSource.UpdateCollectionByIndex(index, dataItem, false);
|
|
||||||
|
|
||||||
//_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()
|
private Task SaveChangesToServerAsync()
|
||||||
|
|
@ -431,6 +591,27 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
||||||
e.Column.Visible = AcDomain.IsDeveloperVersion;
|
e.Column.Visible = AcDomain.IsDeveloperVersion;
|
||||||
e.Column.ShowInColumnChooser = 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)
|
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.DataItemDeleting = EventCallback.Factory.Create<GridDataItemDeletingEventArgs>(this, OnItemDeleting);
|
||||||
base.EditModelSaving = EventCallback.Factory.Create<GridEditModelSavingEventArgs>(this, OnItemSaving);
|
base.EditModelSaving = EventCallback.Factory.Create<GridEditModelSavingEventArgs>(this, OnItemSaving);
|
||||||
base.CustomizeEditModel = EventCallback.Factory.Create<GridCustomizeEditModelEventArgs>(this, OnCustomizeEditModel);
|
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.EditStart = EventCallback.Factory.Create<GridEditStartEventArgs>(this, OnEditStart);
|
||||||
|
base.EditCanceling = EventCallback.Factory.Create<GridEditCancelingEventArgs>(this, OnEditCanceling);
|
||||||
|
|
||||||
CustomizeElement += OnCustomizeElement;
|
CustomizeElement += OnCustomizeElement;
|
||||||
|
|
||||||
|
|
@ -460,16 +642,6 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
||||||
HighlightRowOnHover = true;
|
HighlightRowOnHover = true;
|
||||||
AutoCollapseDetailRow = true;
|
AutoCollapseDetailRow = true;
|
||||||
AutoExpandAllGroupRows = false;
|
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;
|
IsFirstInitializeParameterCore = true;
|
||||||
}
|
}
|
||||||
|
|
@ -517,6 +689,31 @@ public abstract class MgGridBase<TSignalRDataSource, TDataItem, TId, TLoggerClie
|
||||||
return _dataSource.LoadDataSourceAsync(false);
|
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()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_isDisposed) return;
|
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