Compare commits

...

11 Commits

Author SHA1 Message Date
Loretta 15776ca537 Refactor fullscreen grid UI; add serializer diagnostics/tests
- Replaced DxWindow with custom Bootstrap 5 fullscreen overlay for grid components, improving fullscreen UX and styling.
- Added new CSS for fullscreen overlay, header, and body; retained legacy DxWindow styles for compatibility.
- Introduced SignalRSerializerDiagnosticLog flag to control binary serializer diagnostics at runtime.
- Enabled diagnostics in DevAdminSignalRHub, FruitBankSignalRClient, and Program.cs based on the new flag.
- Updated OrderClientTests to use GetStockTakings(false).
- Added StockTakingSerializerTests for binary serialization/deserialization validation and debugging.
2025-12-20 08:40:03 +01:00
Loretta 017eb16c4b Refactor MgGridInfoPanel for theme, UX, and PDF perf
- Refactored MgGridInfoPanel for DevExpress theme compatibility and improved usability; streamlined HTML/CSS, added OnDataItemChanged event, and enhanced empty state handling.
- Updated CSS to use theme variables, improved responsive grid and table styling, and enhanced integration with DevExpress components.
- GridShippingDocumentInfoPanel now uses OnDataItemChanged to load a random PDF per row selection; table layout and totals improved.
- Optimized pdfViewer.js to cache rendered PDFs, skip redundant renders, and improve logging and error handling.
- Added empty helper classes for future extensibility.
- Minor: MainLayout now uses RefreshMainLayout for UI refresh after auto-login.
2025-12-19 13:59:00 +01:00
Loretta 4c86914884 Add fullscreen grid support and PDF preview in info panel
- Added fullscreen mode to grid and info panel components, including toolbar toggle and fullscreen styling.
- Introduced embedded PDF viewing in the info panel using PDF.js and a custom JavaScript viewer.
- Updated interfaces, CSS, and toolbar templates to support new features.
- Added new PDF asset (2_BANK  FRA.pdf) for document preview.
- Minor: Added local settings for Bash permission, fixed text encoding, and improved info panel table layout.
- No code changes in other referenced PDF files; added for informational or asset purposes only.
2025-12-19 07:15:54 +01:00
Loretta 5255917210 Enhance MgGridInfoPanel with responsive/fixed columns
Add support for both responsive and fixed column layouts in MgGridInfoPanel via new parameters (TwoColumnBreakpoint, ThreeColumnBreakpoint, FourColumnBreakpoint, FixedColumnCount). Refactor CSS to use variables for breakpoints, add fixed column classes, and update container queries. Move styles to a new global mg-grid-info-panel.css, referenced in App.razor and index.html. Improve view mode styling and accessibility. Add Partner.Country column to GridPartner.razor.
2025-12-18 11:41:07 +01:00
Loretta 739d0fa808 Refactor: decouple InfoPanel using MgGridWithInfoPanel
Major refactor to decouple InfoPanel logic from grid base. Introduces MgGridWithInfoPanel wrapper component to manage grid and InfoPanel layout and communication. InfoPanels are now customizable via Razor templates with named slots (header, footer, etc.), and grid-to-InfoPanel communication is routed through the wrapper. Removes legacy C#-only InfoPanel base classes and parameters from grid base. This improves flexibility, composability, and maintainability of grid+InfoPanel UIs.
2025-12-18 11:02:53 +01:00
Loretta 112d633590 Add extensible InfoPanel system with custom templates
Introduce a flexible InfoPanel architecture for grid components, allowing per-grid customization via a new InfoPanelType parameter. Add MgInfoPanelTemplateBase for code-based InfoPanel templates with overridable header, content, and footer sections. Support custom templates in MgGridInfoPanel via new RenderFragment parameters. Add MgGridDataColumn for InfoPanel-specific column settings. Demonstrate usage with a custom InfoPanel for ShippingDocument grid. Maintains backward compatibility with default InfoPanel behavior.
2025-12-18 10:03:32 +01:00
Loretta fe1a59a0bd Refactor grid toolbar and InfoPanel for reusability
Introduce reusable MgGridToolbarBase and MgGridToolbarTemplate components, replacing old toolbar implementations. InfoPanel now displays grid captions and includes a toolbar for quick actions. Refactor InfoPanel value rendering for improved DevExpress-like appearance. Update IMgGridBase and related classes to support captions and use new abstractions. Update usings and project structure accordingly.
2025-12-17 18:31:59 +01:00
Loretta 90f12a160e Refactor InfoPanel: non-generic, nested grid support
- Replace generic InfoPanel with non-generic version using IInfoPanelBase
- Add ParentGrid, GetRootGrid, and InfoPanelInstance to IMgGridBase for nested grid hierarchy
- Only root grid displays InfoPanel; nested grids inherit context
- InfoPanel now handles any data type via reflection and object
- All grid-to-InfoPanel communication routed through root grid
- Add option to show/hide readonly fields in edit mode
- Improve InfoPanel CSS for up to 4 responsive columns
- Remove redundant code and add debug output for InfoPanel data flow
2025-12-17 13:54:07 +01:00
Loretta 109a4b82b4 Refactor grid InfoPanel: sticky, responsive, new icons
- Redesigned MgGridInfoPanel to use a sticky, scroll-aware layout via JavaScript for better UX when scrolling.
- InfoPanel now uses a responsive CSS grid layout with container queries for 1/2/3 column display based on width.
- Added new toolbar icons using SVG masks for a modern, consistent look; updated toolbar item class names.
- Added "Prev Row" and "Next Row" navigation buttons to the grid toolbar, with corresponding methods in grid base classes.
- Unified edit state enum naming to MgGridEditState and updated all references.
- Improved InfoPanel cell rendering for better text overflow handling and tooltips.
- Updated CSS for InfoPanel and grid, including sticky pane support and icon styles.
- Registered mgGridInfoPanel.js in App.razor and index.html for JS interop.
- Minor UI/UX tweaks: InfoPanel header, background colors, and panel sizing.
2025-12-17 10:20:17 +01:00
Loretta 45294199cf Refactor grid editing, info panel, and toolbar system
- Introduce MgEditState enum and expose EditState on IMgGridBase
- Replace event-based syncing state with property-based state
- Redesign MgGridInfoPanel to support both view and edit modes with dynamic DevExpress editors and two-way binding
- Add visual distinction for edit/view modes in info panel
- Replace FruitBankToolbarTemplate with generic MgGridToolbarTemplate; toolbar adapts to grid edit/sync state
- Update all grid usages to use new toolbar
- Improve robustness, error handling, and maintainability throughout grid, info panel, and toolbar code
2025-12-17 06:21:21 +01:00
Loretta c1cf30b8f0 Add dynamic Info Panel to grids and update app splash image
Introduced a dynamic Info Panel to MgGridBase, allowing users to view details of the selected row in a side panel. Added the MgGridInfoPanel component with automatic form generation and styling. Updated all grid usages to use the new OnGridFocusedRowChanged event and enabled the info panel in GridPartner. Changed app logo and splash screen references in the Windows packaging manifest and added a placeholder splash image. Also included minor using fixes.
2025-12-16 16:12:38 +01:00
15 changed files with 1950 additions and 47 deletions

View File

@ -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
}

View File

@ -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;

View File

@ -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;
}

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace AyCode.Blazor.Components.Components.Grids
{
internal class MgGridHelper
{
}
}

View File

@ -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();
};
}
}

View File

@ -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
}
}

View File

@ -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;
}

View File

@ -0,0 +1,6 @@
namespace AyCode.Blazor.Components.Components.Grids;
public class MgGridInfoPanelHelper
{
}

View File

@ -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;
}
}

View File

@ -0,0 +1,6 @@
namespace AyCode.Blazor.Components.Components.Grids;
public class MgGridToolbarHelper
{
}

View File

@ -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);
}
}

View File

@ -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
}
};
}

View File

@ -0,0 +1,6 @@
namespace AyCode.Blazor.Components.Components;
public class MgComponentsHelper
{
}

View File

@ -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';
}

View File

@ -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;
}
};